google-cloud-storage 1.25.1 → 1.26.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ab43474a1a6a25a439d96a67b8664caff72861d3d121c927d5c11623ae570cab
4
- data.tar.gz: f2e6378f9708fc079f2ce3539947e5587eb57263b2d96c8877c93d1b07b30abd
3
+ metadata.gz: 6a69b7d03384d604c8b96548b952b6894af09ee7cef01845fe92b998ea77bcc8
4
+ data.tar.gz: 9fd54043ddab11b2284c73b17e780877b795a59593943deef43b9d14fbb4da1e
5
5
  SHA512:
6
- metadata.gz: d40dfe073d42be6d3098a5c5e8779c4c52a7407b77f159e435e8c76ffe1cc59fc037591bcbc1b00ebaea9763d6085c7a9e639549eaacd89ea1444c1cd81a75e3
7
- data.tar.gz: 16c8f515149d0794c40a474fb43d6c1859ff427b1ab5c75c6f4610959c49e3ed1a2c8d3a0060be58ded02f34867437fcaf81c1e2a8e00a8320af2fff470e5b34
6
+ metadata.gz: 36d6b3d59ab2655020f97e7d15a33a35b48a541632e03a6a8aa01f20289c821b9582cf21fdea2799cad8111c8208bd866154246a8ec69fb33102fd78853bc49f
7
+ data.tar.gz: 8d59996b1108a4c44e074e9762e48ad4a1f93c8dfd07efa11b77472a3b2285be52ce1486afa76b5301de683cf918c23d41d1d0dd877cd4bbaf0ab3d1cc91e405
@@ -1,5 +1,22 @@
1
1
  # Release History
2
2
 
3
+ ### 1.26.0 / 2020-04-06
4
+
5
+ #### Features
6
+
7
+ * Update V4 Signature support in Project#signed_url, Bucket#signed_url and File#signed_url
8
+ * Add scheme, virtual_hosted_style and bucket_bound_hostname to #signed_url methods
9
+ * Add support for V4 query param encoding and ordering
10
+ * Convert tabs in V4 to single whitespace character
11
+ * Set payload in V4 to X-Goog-Content-SHA256 if present
12
+ * Fix method param default value GET for #signed_url
13
+ * Add support for V4 Signature POST Policies
14
+ * Add Bucket#generate_signed_post_policy_v4
15
+
16
+ #### Bug Fixes
17
+
18
+ * Address keyword argument warnings in Ruby 2.7 and later
19
+
3
20
  ### 1.25.1 / 2020-01-06
4
21
 
5
22
  #### Documentation
@@ -1254,22 +1254,27 @@ module Google
1254
1254
  storage_class: nil, encryption_key: nil, kms_key: nil,
1255
1255
  temporary_hold: nil, event_based_hold: nil
1256
1256
  ensure_service!
1257
- options = { acl: File::Acl.predefined_rule_for(acl), md5: md5,
1258
- cache_control: cache_control, content_type: content_type,
1259
- content_disposition: content_disposition, crc32c: crc32c,
1260
- content_encoding: content_encoding, metadata: metadata,
1261
- content_language: content_language, key: encryption_key,
1262
- kms_key: kms_key,
1263
- storage_class: storage_class_for(storage_class),
1264
- temporary_hold: temporary_hold,
1265
- event_based_hold: event_based_hold,
1266
- user_project: user_project }
1267
1257
  ensure_io_or_file_exists! file
1268
1258
  path ||= file.path if file.respond_to? :path
1269
1259
  path ||= file if file.is_a? String
1270
1260
  raise ArgumentError, "must provide path" if path.nil?
1271
1261
 
1272
- gapi = service.insert_file name, file, path, options
1262
+
1263
+ gapi = service.insert_file name, file, path, acl: File::Acl.predefined_rule_for(acl),
1264
+ md5: md5,
1265
+ cache_control: cache_control,
1266
+ content_type: content_type,
1267
+ content_disposition: content_disposition,
1268
+ crc32c: crc32c,
1269
+ content_encoding: content_encoding,
1270
+ metadata: metadata,
1271
+ content_language: content_language,
1272
+ key: encryption_key,
1273
+ kms_key: kms_key,
1274
+ storage_class: storage_class_for(storage_class),
1275
+ temporary_hold: temporary_hold,
1276
+ event_based_hold: event_based_hold,
1277
+ user_project: user_project
1273
1278
  File.from_gapi gapi, service, user_project: user_project
1274
1279
  end
1275
1280
  alias upload_file create_file
@@ -1368,9 +1373,6 @@ module Google
1368
1373
  raise ArgumentError, "must provide at least two source files"
1369
1374
  end
1370
1375
 
1371
- options = { acl: File::Acl.predefined_rule_for(acl),
1372
- key: encryption_key,
1373
- user_project: user_project }
1374
1376
  destination_gapi = nil
1375
1377
  if block_given?
1376
1378
  destination_gapi = API::Object.new
@@ -1378,8 +1380,11 @@ module Google
1378
1380
  yield updater
1379
1381
  updater.check_for_changed_metadata!
1380
1382
  end
1381
- gapi = service.compose_file name, sources, destination,
1382
- destination_gapi, options
1383
+
1384
+ acl_rule = File::Acl.predefined_rule_for acl
1385
+ gapi = service.compose_file name, sources, destination, destination_gapi, acl: acl_rule,
1386
+ key: encryption_key,
1387
+ user_project: user_project
1383
1388
  File.from_gapi gapi, service, user_project: user_project
1384
1389
  end
1385
1390
  alias compose_file compose
@@ -1440,6 +1445,19 @@ module Google
1440
1445
  # using the URL, but only when the file resource is missing the
1441
1446
  # corresponding values. (These values can be permanently set using
1442
1447
  # {File#content_disposition=} and {File#content_type=}.)
1448
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
1449
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
1450
+ # hostname, which adds the bucket into the host portion of the URI rather
1451
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
1452
+ # For V4 signing, this also sets the `host` header in the canonicalized
1453
+ # extension headers to the virtual hosted-style host, unless that header is
1454
+ # supplied via the `headers` param. The default value of `false` uses the
1455
+ # form of `https://storage.googleapis.com/mybucket`.
1456
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
1457
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
1458
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
1459
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
1460
+ # `my-load-balancer-domain.tld`.
1443
1461
  # @param [Symbol, String] version The version of the signed credential
1444
1462
  # to create. Must be one of `:v2` or `:v4`. The default value is
1445
1463
  # `:v2`.
@@ -1510,28 +1528,49 @@ module Google
1510
1528
  # bucket = storage.bucket "my-todo-app"
1511
1529
  # list_files_url = bucket.signed_url version: :v4
1512
1530
  #
1513
- def signed_url path = nil, method: nil, expires: nil, content_type: nil,
1514
- content_md5: nil, headers: nil, issuer: nil,
1515
- client_email: nil, signing_key: nil, private_key: nil,
1516
- query: nil, version: nil
1531
+ def signed_url path = nil,
1532
+ method: "GET",
1533
+ expires: nil,
1534
+ content_type: nil,
1535
+ content_md5: nil,
1536
+ headers: nil,
1537
+ issuer: nil,
1538
+ client_email: nil,
1539
+ signing_key: nil,
1540
+ private_key: nil,
1541
+ query: nil,
1542
+ scheme: "HTTPS",
1543
+ virtual_hosted_style: nil,
1544
+ bucket_bound_hostname: nil,
1545
+ version: nil
1517
1546
  ensure_service!
1518
1547
  version ||= :v2
1519
1548
  case version.to_sym
1520
1549
  when :v2
1521
1550
  signer = File::SignerV2.from_bucket self, path
1522
- signer.signed_url method: method, expires: expires,
1523
- headers: headers, content_type: content_type,
1524
- content_md5: content_md5, issuer: issuer,
1551
+ signer.signed_url method: method,
1552
+ expires: expires,
1553
+ headers: headers,
1554
+ content_type: content_type,
1555
+ content_md5: content_md5,
1556
+ issuer: issuer,
1525
1557
  client_email: client_email,
1526
1558
  signing_key: signing_key,
1527
- private_key: private_key, query: query
1559
+ private_key: private_key,
1560
+ query: query
1528
1561
  when :v4
1529
1562
  signer = File::SignerV4.from_bucket self, path
1530
- signer.signed_url method: method, expires: expires,
1531
- headers: headers, issuer: issuer,
1563
+ signer.signed_url method: method,
1564
+ expires: expires,
1565
+ headers: headers,
1566
+ issuer: issuer,
1532
1567
  client_email: client_email,
1533
1568
  signing_key: signing_key,
1534
- private_key: private_key, query: query
1569
+ private_key: private_key,
1570
+ query: query,
1571
+ scheme: scheme,
1572
+ virtual_hosted_style: virtual_hosted_style,
1573
+ bucket_bound_hostname: bucket_bound_hostname
1535
1574
  else
1536
1575
  raise ArgumentError, "version '#{version}' not supported"
1537
1576
  end
@@ -1558,12 +1597,13 @@ module Google
1558
1597
  #
1559
1598
  # @param [String] path Path to the file in Google Cloud Storage.
1560
1599
  # @param [Hash] policy The security policy that describes what
1561
- # can and cannot be uploaded in the form. When provided,
1562
- # the PostObject fields will include a Signature based on the JSON
1563
- # representation of this Hash and the same policy in Base64 format.
1600
+ # can and cannot be uploaded in the form. When provided, the PostObject
1601
+ # fields will include a signature based on the JSON representation of
1602
+ # this hash and the same policy in Base64 format.
1603
+ #
1564
1604
  # If you do not provide a security policy, requests are considered
1565
1605
  # to be anonymous and will only work with buckets that have granted
1566
- # WRITE or FULL_CONTROL permission to anonymous users.
1606
+ # `WRITE` or `FULL_CONTROL` permission to anonymous users.
1567
1607
  # See [Policy Document](https://cloud.google.com/storage/docs/xml-api/post-object#policydocument)
1568
1608
  # for more information.
1569
1609
  # @param [String] issuer Service Account's Client Email.
@@ -1633,17 +1673,110 @@ module Google
1633
1673
  # post.fields[:signature] #=> "ABC...XYZ="
1634
1674
  # post.fields[:policy] #=> "ABC...XYZ="
1635
1675
  #
1636
- def post_object path, policy: nil, issuer: nil,
1637
- client_email: nil, signing_key: nil,
1676
+ def post_object path,
1677
+ policy: nil,
1678
+ issuer: nil,
1679
+ client_email: nil,
1680
+ signing_key: nil,
1638
1681
  private_key: nil
1639
1682
  ensure_service!
1640
-
1641
1683
  signer = File::SignerV2.from_bucket self, path
1642
- signer.post_object issuer: issuer, client_email: client_email,
1643
- signing_key: signing_key, private_key: private_key,
1684
+ signer.post_object issuer: issuer,
1685
+ client_email: client_email,
1686
+ signing_key: signing_key,
1687
+ private_key: private_key,
1644
1688
  policy: policy
1645
1689
  end
1646
1690
 
1691
+ ##
1692
+ # Generate a PostObject that includes the fields and url to
1693
+ # upload objects via html forms.
1694
+ #
1695
+ # Generating a PostObject requires service account credentials,
1696
+ # either by connecting with a service account when calling
1697
+ # {Google::Cloud.storage}, or by passing in the service account
1698
+ # `issuer` and `signing_key` values. Although the private key can
1699
+ # be passed as a string for convenience, creating and storing
1700
+ # an instance of `OpenSSL::PKey::RSA` is more efficient
1701
+ # when making multiple calls to `generate_signed_post_policy_v4`.
1702
+ #
1703
+ # A {SignedUrlUnavailable} is raised if the service account credentials
1704
+ # are missing. Service account credentials are acquired by following the
1705
+ # steps in [Service Account Authentication](
1706
+ # https://cloud.google.com/storage/docs/authentication#service_accounts).
1707
+ #
1708
+ # @see https://cloud.google.com/storage/docs/xml-api/post-object
1709
+ #
1710
+ # @param [String] path Path to the file in Google Cloud Storage.
1711
+ # @param [String] issuer Service Account's Client Email.
1712
+ # @param [String] client_email Service Account's Client Email.
1713
+ # @param [OpenSSL::PKey::RSA, String] signing_key Service Account's
1714
+ # Private Key.
1715
+ # @param [OpenSSL::PKey::RSA, String] private_key Service Account's
1716
+ # Private Key.
1717
+ # @param [Integer] expires The number of seconds until the URL expires.
1718
+ # The default is 604800 (7 days).
1719
+ # @param [Hash] fields User-supplied form fields such as `acl`,
1720
+ # `cache-control`, `success_action_status`, and `success_action_redirect`.
1721
+ # @param [Array<Hash|Array>] conditions User-supplied policy conditions.
1722
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
1723
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
1724
+ # hostname, which adds the bucket into the host portion of the URI rather
1725
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
1726
+ # The default value of `false` uses the
1727
+ # form of `https://storage.googleapis.com/mybucket`.
1728
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
1729
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
1730
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
1731
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
1732
+ # `my-load-balancer-domain.tld`.
1733
+ #
1734
+ # @return [PostObject] An object containing the URL, fields, and values needed to upload files via html forms.
1735
+ #
1736
+ # @example
1737
+ # require "google/cloud/storage"
1738
+ #
1739
+ # storage = Google::Cloud::Storage.new
1740
+ #
1741
+ # bucket = storage.bucket "my-todo-app"
1742
+ #
1743
+ # conditions = [["starts-with", "$acl","public"]]
1744
+ # post = bucket.generate_signed_post_policy_v4 "avatars/heidi/400x400.png", expires: 10,
1745
+ # conditions: conditions
1746
+ #
1747
+ # post.url #=> "https://storage.googleapis.com/my-todo-app/"
1748
+ # post.fields["key"] #=> "my-todo-app/avatars/heidi/400x400.png"
1749
+ # post.fields["policy"] #=> "ABC...XYZ"
1750
+ # post.fields["x-goog-algorithm"] #=> "GOOG4-RSA-SHA256"
1751
+ # post.fields["x-goog-credential"] #=> "cred@pid.iam.gserviceaccount.com/20200123/auto/storage/goog4_request"
1752
+ # post.fields["x-goog-date"] #=> "20200128T000000Z"
1753
+ # post.fields["x-goog-signature"] #=> "4893a0e...cd82"
1754
+ #
1755
+ def generate_signed_post_policy_v4 path,
1756
+ issuer: nil,
1757
+ client_email: nil,
1758
+ signing_key: nil,
1759
+ private_key: nil,
1760
+ expires: nil,
1761
+ fields: nil,
1762
+ conditions: nil,
1763
+ scheme: "https",
1764
+ virtual_hosted_style: nil,
1765
+ bucket_bound_hostname: nil
1766
+ ensure_service!
1767
+ signer = File::SignerV4.from_bucket self, path
1768
+ signer.post_object issuer: issuer,
1769
+ client_email: client_email,
1770
+ signing_key: signing_key,
1771
+ private_key: private_key,
1772
+ expires: expires,
1773
+ fields: fields,
1774
+ conditions: conditions,
1775
+ scheme: scheme,
1776
+ virtual_hosted_style: virtual_hosted_style,
1777
+ bucket_bound_hostname: bucket_bound_hostname
1778
+ end
1779
+
1647
1780
  ##
1648
1781
  # The {Bucket::Acl} instance used to control access to the bucket.
1649
1782
  #
@@ -2125,11 +2258,12 @@ module Google
2125
2258
  def create_notification topic, custom_attrs: nil, event_types: nil,
2126
2259
  prefix: nil, payload: nil
2127
2260
  ensure_service!
2128
- options = { custom_attrs: custom_attrs, event_types: event_types,
2129
- prefix: prefix, payload: payload,
2130
- user_project: user_project }
2131
2261
 
2132
- gapi = service.insert_notification name, topic, options
2262
+ gapi = service.insert_notification name, topic, custom_attrs: custom_attrs,
2263
+ event_types: event_types,
2264
+ prefix: prefix,
2265
+ payload: payload,
2266
+ user_project: user_project
2133
2267
  Notification.from_gapi name, gapi, service, user_project: user_project
2134
2268
  end
2135
2269
  alias new_notification create_notification
@@ -2215,7 +2349,7 @@ module Google
2215
2349
  patch_args = Hash[attributes.map do |attr|
2216
2350
  [attr, @gapi.send(attr)]
2217
2351
  end]
2218
- patch_gapi = API::Bucket.new patch_args
2352
+ patch_gapi = API::Bucket.new(**patch_args)
2219
2353
  @gapi = service.patch_bucket name, patch_gapi,
2220
2354
  user_project: user_project
2221
2355
  @lazy = nil
@@ -1479,6 +1479,19 @@ module Google
1479
1479
  # using the URL, but only when the file resource is missing the
1480
1480
  # corresponding values. (These values can be permanently set using
1481
1481
  # {#content_disposition=} and {#content_type=}.)
1482
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
1483
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
1484
+ # hostname, which adds the bucket into the host portion of the URI rather
1485
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
1486
+ # For V4 signing, this also sets the `host` header in the canonicalized
1487
+ # extension headers to the virtual hosted-style host, unless that header is
1488
+ # supplied via the `headers` param. The default value of `false` uses the
1489
+ # form of `https://storage.googleapis.com/mybucket`.
1490
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
1491
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
1492
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
1493
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
1494
+ # `my-load-balancer-domain.tld`.
1482
1495
  # @param [Symbol, String] version The version of the signed credential
1483
1496
  # to create. Must be one of `:v2` or `:v4`. The default value is
1484
1497
  # `:v2`.
@@ -1503,7 +1516,7 @@ module Google
1503
1516
  # file = bucket.file "avatars/heidi/400x400.png"
1504
1517
  # shared_url = file.signed_url expires: 300, # 5 minutes from now
1505
1518
  # version: :v4
1506
-
1519
+ #
1507
1520
  # @example Using the `issuer` and `signing_key` options:
1508
1521
  # require "google/cloud/storage"
1509
1522
  #
@@ -1543,28 +1556,48 @@ module Google
1543
1556
  # # Send the `x-goog-resumable:start` header and the content type
1544
1557
  # # with the resumable upload POST request.
1545
1558
  #
1546
- def signed_url method: nil, expires: nil, content_type: nil,
1547
- content_md5: nil, headers: nil, issuer: nil,
1548
- client_email: nil, signing_key: nil, private_key: nil,
1549
- query: nil, version: nil
1559
+ def signed_url method: "GET",
1560
+ expires: nil,
1561
+ content_type: nil,
1562
+ content_md5: nil,
1563
+ headers: nil,
1564
+ issuer: nil,
1565
+ client_email: nil,
1566
+ signing_key: nil,
1567
+ private_key: nil,
1568
+ query: nil,
1569
+ scheme: "HTTPS",
1570
+ virtual_hosted_style: nil,
1571
+ bucket_bound_hostname: nil,
1572
+ version: nil
1550
1573
  ensure_service!
1551
1574
  version ||= :v2
1552
1575
  case version.to_sym
1553
1576
  when :v2
1554
1577
  signer = File::SignerV2.from_file self
1555
- signer.signed_url method: method, expires: expires,
1556
- headers: headers, content_type: content_type,
1557
- content_md5: content_md5, issuer: issuer,
1578
+ signer.signed_url method: method,
1579
+ expires: expires,
1580
+ headers: headers,
1581
+ content_type: content_type,
1582
+ content_md5: content_md5,
1583
+ issuer: issuer,
1558
1584
  client_email: client_email,
1559
1585
  signing_key: signing_key,
1560
- private_key: private_key, query: query
1586
+ private_key: private_key,
1587
+ query: query
1561
1588
  when :v4
1562
1589
  signer = File::SignerV4.from_file self
1563
- signer.signed_url method: method, expires: expires,
1564
- headers: headers, issuer: issuer,
1590
+ signer.signed_url method: method,
1591
+ expires: expires,
1592
+ headers: headers,
1593
+ issuer: issuer,
1565
1594
  client_email: client_email,
1566
1595
  signing_key: signing_key,
1567
- private_key: private_key, query: query
1596
+ private_key: private_key,
1597
+ query: query,
1598
+ scheme: scheme,
1599
+ virtual_hosted_style: virtual_hosted_style,
1600
+ bucket_bound_hostname: bucket_bound_hostname
1568
1601
  else
1569
1602
  raise ArgumentError, "version '#{version}' not supported"
1570
1603
  end
@@ -1733,7 +1766,7 @@ module Google
1733
1766
  # Sending nil metadata results in an Apiary runtime error:
1734
1767
  # NoMethodError: undefined method `each' for nil:NilClass
1735
1768
  attr_params.reject! { |k, v| k == :metadata && v.nil? }
1736
- Google::Apis::StorageV1::Object.new attr_params
1769
+ Google::Apis::StorageV1::Object.new(**attr_params)
1737
1770
  end
1738
1771
 
1739
1772
  protected
@@ -1784,12 +1817,12 @@ module Google
1784
1817
  user_project: user_project }.delete_if { |_k, v| v.nil? }
1785
1818
 
1786
1819
  resp = service.rewrite_file \
1787
- bucket, name, new_bucket, new_name, updated_gapi, options
1820
+ bucket, name, new_bucket, new_name, updated_gapi, **options
1788
1821
  until resp.done
1789
1822
  sleep 1
1790
1823
  retry_options = options.merge token: resp.rewrite_token
1791
1824
  resp = service.rewrite_file \
1792
- bucket, name, new_bucket, new_name, updated_gapi, retry_options
1825
+ bucket, name, new_bucket, new_name, updated_gapi, **retry_options
1793
1826
  end
1794
1827
  resp.resource
1795
1828
  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
@@ -39,37 +39,83 @@ module Google
39
39
  new bucket.name, file_name, bucket.service
40
40
  end
41
41
 
42
- def signed_url method: "GET", expires: nil, headers: nil,
43
- issuer: nil, client_email: nil, signing_key: nil,
44
- private_key: nil, query: nil
45
- issuer, signer = issuer_and_signer issuer, client_email,
46
- signing_key, private_key
42
+ def post_object issuer: nil,
43
+ client_email: nil,
44
+ signing_key: nil,
45
+ private_key: nil,
46
+ expires: nil,
47
+ fields: nil,
48
+ conditions: nil,
49
+ scheme: "https",
50
+ virtual_hosted_style: nil,
51
+ bucket_bound_hostname: nil
52
+ i = determine_issuer issuer, client_email
53
+ s = determine_signing_key signing_key, private_key
54
+ raise SignedUrlUnavailable unless i && s
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
+
67
+ policy_str = escape_characters p.to_json
68
+
69
+ policy = Base64.strict_encode64(policy_str).force_encoding "utf-8"
70
+ signature = generate_signature s, policy
71
+
72
+ post_fields["x-goog-signature"] = signature
73
+ post_fields["policy"] = policy
74
+ url = post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
75
+ hostname = "#{url}#{bucket_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
76
+ Google::Cloud::Storage::PostObject.new hostname, post_fields
77
+ end
78
+
79
+ def signed_url method: "GET",
80
+ expires: nil,
81
+ headers: nil,
82
+ issuer: nil,
83
+ client_email: nil,
84
+ signing_key: nil,
85
+ private_key: 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
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
- canonical_and_signed_headers headers
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 = CGI.escape issuer + "/" + scope
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
- ext_path,
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
- "UNSIGNED-PAYLOAD"].join("\n")
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,33 +124,85 @@ module Google
78
124
  signature = signer.call string_to_sign
79
125
 
80
126
  # Construct signed URL
81
- "#{ext_url}?#{canonical_query_str}&X-Goog-Signature=#{signature}"
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
+ def escape_characters str
133
+ str.split("").map do |s|
134
+ if !s.ascii_only?
135
+ escape_special_unicode s
136
+ else
137
+ case s
138
+ when "\\"
139
+ '\\'
140
+ when "\b"
141
+ '\b'
142
+ when "\f"
143
+ '\f'
144
+ when "\n"
145
+ '\n'
146
+ when "\r"
147
+ '\r'
148
+ when "\t"
149
+ '\t'
150
+ when "\v"
151
+ '\v'
152
+ else
153
+ s
154
+ end
155
+ end
156
+ end.join
157
+ end
158
+
159
+ def escape_special_unicode str
160
+ str.unpack("U*").map { |i| '\u' + i.to_s(16).rjust(4, "0") }.join
82
161
  end
83
162
 
84
163
  protected
85
164
 
165
+ def required_fields issuer, time
166
+ {
167
+ "key" => @file_name,
168
+ "x-goog-date" => time.strftime("%Y%m%dT%H%M%SZ"),
169
+ "x-goog-credential" => "#{issuer}/#{time.strftime '%Y%m%d'}/auto/storage/goog4_request",
170
+ "x-goog-algorithm" => "GOOG4-RSA-SHA256"
171
+ }.freeze
172
+ end
173
+
174
+ def policy_conditions base_fields, user_conditions, user_fields
175
+ # Convert each pair in base_fields hash to a single-entry hash in an array.
176
+ conditions = base_fields.to_a.map { |f| Hash[*f] }
177
+ # Add user-provided conditions to the head of the conditions array.
178
+ conditions.unshift user_conditions if user_conditions && !user_conditions.empty?
179
+ if user_fields
180
+ # Convert each pair in fields hash to a single-entry hash and add it to the head of the conditions array.
181
+ user_fields.to_a.reverse.each { |f| conditions.unshift Hash[*f] }
182
+ end
183
+ conditions.freeze
184
+ end
185
+
186
+ def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
187
+ url = ext_url scheme, virtual_hosted_style, bucket_bound_hostname
188
+ "#{url}#{file_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
189
+ end
190
+
86
191
  def determine_issuer issuer, client_email
87
192
  # Parse the Service Account and get client id and private key
88
193
  issuer = issuer || client_email || @service.credentials.issuer
89
- unless issuer
90
- raise SignedUrlUnavailable, "issuer (client_email) missing"
91
- end
194
+ raise SignedUrlUnavailable, "issuer (client_email) missing" unless issuer
92
195
  issuer
93
196
  end
94
197
 
95
198
  def determine_signing_key signing_key, private_key
96
- signing_key = signing_key || private_key ||
97
- @service.credentials.signing_key
98
- unless signing_key
99
- raise SignedUrlUnavailable, "signing_key (private_key) missing"
100
- end
199
+ signing_key = signing_key || private_key || @service.credentials.signing_key
200
+ raise SignedUrlUnavailable, "signing_key (private_key) missing" unless signing_key
101
201
  signing_key
102
202
  end
103
203
 
104
204
  def service_account_signer signer
105
- unless signer.respond_to? :sign
106
- signer = OpenSSL::PKey::RSA.new signer
107
- end
205
+ signer = OpenSSL::PKey::RSA.new signer unless signer.respond_to? :sign
108
206
  # Sign string to sign
109
207
  lambda do |string_to_sign|
110
208
  sig = signer.sign OpenSSL::Digest::SHA256.new, string_to_sign
@@ -119,25 +217,22 @@ module Google
119
217
  [issuer, signer]
120
218
  end
121
219
 
122
- def canonical_and_signed_headers headers
123
- # Headers needs to be in alpha order.
220
+ def canonical_and_signed_headers headers, virtual_hosted_style, bucket_bound_hostname
221
+ if virtual_hosted_style && bucket_bound_hostname
222
+ raise "virtual_hosted_style: #{virtual_hosted_style} and bucket_bound_hostname: " \
223
+ "#{bucket_bound_hostname} params cannot both be passed together"
224
+ end
225
+
124
226
  canonical_headers = headers || {}
125
227
  headers_arr = canonical_headers.map do |k, v|
126
- [k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, "\t")]
228
+ [k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, " ")]
127
229
  end
128
230
  canonical_headers = Hash[headers_arr]
129
- canonical_headers["host"] = "storage.googleapis.com"
130
-
131
- canonical_headers = canonical_headers.sort_by do |k, _|
132
- k.downcase
133
- end.to_h
134
- canonical_headers_str = ""
135
- canonical_headers.each do |k, v|
136
- canonical_headers_str += "#{k}:#{v}\n"
137
- end
138
- signed_headers_str = ""
139
- canonical_headers.each_key { |k| signed_headers_str += "#{k};" }
140
- signed_headers_str = signed_headers_str.chomp ";"
231
+ canonical_headers["host"] = host_name virtual_hosted_style, bucket_bound_hostname
232
+
233
+ canonical_headers = canonical_headers.sort_by(&:first).to_h
234
+ canonical_headers_str = canonical_headers.map { |k, v| "#{k}:#{v}\n" }.join
235
+ signed_headers_str = canonical_headers.keys.join ";"
141
236
  [canonical_headers_str, signed_headers_str]
142
237
  end
143
238
 
@@ -149,32 +244,103 @@ module Google
149
244
  expires
150
245
  end
151
246
 
152
- def canonical_query query, algorithm, credential, goog_date, expires,
153
- signed_headers_str
247
+ def canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
154
248
  query ||= {}
155
249
  query["X-Goog-Algorithm"] = algorithm
156
250
  query["X-Goog-Credential"] = credential
157
251
  query["X-Goog-Date"] = goog_date
158
252
  query["X-Goog-Expires"] = expires
159
- query["X-Goog-SignedHeaders"] = CGI.escape signed_headers_str
160
- query = query.sort_by { |k, _| k.to_s.downcase }.to_h
161
- canonical_query_str = ""
162
- query.each { |k, v| canonical_query_str += "#{k}=#{v}&" }
163
- canonical_query_str.chomp "&"
253
+ query["X-Goog-SignedHeaders"] = signed_headers_str
254
+ query = query.map { |k, v| [escape_query_param(k), escape_query_param(v)] }.sort_by(&:first).to_h
255
+ query.map { |k, v| "#{k}=#{v}" }.join "&"
256
+ end
257
+
258
+ ##
259
+ # Only the characters in the regex set [A-Za-z0-9.~_-] must be left un-escaped; all others must be
260
+ # percent-encoded using %XX UTF-8 style.
261
+ def escape_query_param str
262
+ CGI.escape(str.to_s).gsub("%7E", "~")
263
+ end
264
+
265
+ def host_name virtual_hosted_style, bucket_bound_hostname
266
+ return bucket_bound_hostname if bucket_bound_hostname
267
+ virtual_hosted_style ? "#{@bucket_name}.storage.googleapis.com" : "storage.googleapis.com"
164
268
  end
165
269
 
166
270
  ##
167
271
  # The URI-encoded (percent encoded) external path to the file.
168
- def ext_path
169
- path = "/#{@bucket_name}"
170
- path += "/#{String(@file_name)}" if @file_name && !@file_name.empty?
171
- Addressable::URI.escape path
272
+ def file_path path_style
273
+ path = []
274
+ path << "/#{@bucket_name}" if path_style
275
+ path << "/#{String(@file_name)}" if @file_name && !@file_name.empty?
276
+ CGI.escape(path.join).gsub "%2F", "/"
277
+ end
278
+
279
+ ##
280
+ # The external path to the bucket, with trailing slash.
281
+ def bucket_path path_style
282
+ return "/#{@bucket_name}/" if path_style
172
283
  end
173
284
 
174
285
  ##
175
286
  # The external url to the file.
176
- def ext_url
177
- "#{GOOGLEAPIS_URL}#{ext_path}"
287
+ def ext_url scheme, virtual_hosted_style, bucket_bound_hostname
288
+ url = GOOGLEAPIS_URL.dup
289
+ if virtual_hosted_style
290
+ parts = url.split "//"
291
+ parts[1] = "#{@bucket_name}.#{parts[1]}"
292
+ parts.join "//"
293
+ elsif bucket_bound_hostname
294
+ raise ArgumentError, "scheme is required" unless scheme
295
+ URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}"
296
+ else
297
+ url
298
+ end
299
+ end
300
+
301
+ def path_style? virtual_hosted_style, bucket_bound_hostname
302
+ !(virtual_hosted_style || bucket_bound_hostname)
303
+ end
304
+
305
+ ##
306
+ # The external path to the file, URI-encoded.
307
+ # Will not URI encode the special `${filename}` variable.
308
+ # "You can also use the ${filename} variable..."
309
+ # https://cloud.google.com/storage/docs/xml-api/post-object
310
+ #
311
+ def post_object_ext_path
312
+ path = "/#{@bucket_name}/#{@file_name}"
313
+ escaped = Addressable::URI.escape path
314
+ special_var = "${filename}"
315
+ # Restore the unencoded `${filename}` variable, if present.
316
+ if path.include? special_var
317
+ return escaped.gsub "$%7Bfilename%7D", special_var
318
+ end
319
+ escaped
320
+ end
321
+
322
+ ##
323
+ # The external url to the file.
324
+ def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
325
+ url = GOOGLEAPIS_URL.dup
326
+ if virtual_hosted_style
327
+ parts = url.split "//"
328
+ parts[1] = "#{@bucket_name}.#{parts[1]}/"
329
+ parts.join "//"
330
+ elsif bucket_bound_hostname
331
+ raise ArgumentError, "scheme is required" unless scheme
332
+ URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}/"
333
+ else
334
+ url
335
+ end
336
+ end
337
+
338
+ def generate_signature signing_key, data
339
+ unless signing_key.respond_to? :sign
340
+ signing_key = OpenSSL::PKey::RSA.new signing_key
341
+ end
342
+ signature = signing_key.sign OpenSSL::Digest::SHA256.new, data
343
+ signature.unpack("H*").first.force_encoding "utf-8"
178
344
  end
179
345
  end
180
346
  end
@@ -230,11 +230,12 @@ module Google
230
230
  ##
231
231
  # @private
232
232
  def to_gapi
233
- Google::Apis::StorageV1::Policy::Binding.new({
233
+ params = {
234
234
  role: @role,
235
235
  members: @members,
236
236
  condition: @condition&.to_gapi
237
- }.delete_if { |_, v| v.nil? })
237
+ }.delete_if { |_, v| v.nil? }
238
+ Google::Apis::StorageV1::Policy::Binding.new(**params)
238
239
  end
239
240
  end
240
241
  end
@@ -27,7 +27,7 @@ module Google
27
27
  # form. Each key/value pair should be set as an input tag's name and
28
28
  # value.
29
29
  #
30
- # @example
30
+ # @example Using Bucket#post_object (V2):
31
31
  # require "google/cloud/storage"
32
32
  #
33
33
  # storage = Google::Cloud::Storage.new
@@ -41,6 +41,23 @@ module Google
41
41
  # post.fields[:signature] #=> "ABC...XYZ="
42
42
  # post.fields[:policy] #=> "ABC...XYZ="
43
43
  #
44
+ # @example Using Bucket#generate_signed_post_policy_v4 (V4):
45
+ # require "google/cloud/storage"
46
+ #
47
+ # storage = Google::Cloud::Storage.new
48
+ #
49
+ # bucket = storage.bucket "my-todo-app"
50
+ # conditions = [["starts-with","$acl","public"]]
51
+ # post = bucket.generate_signed_post_policy_v4 "avatars/heidi/400x400.png", expires: 10, conditions: conditions
52
+ #
53
+ # post.url #=> "https://storage.googleapis.com/my-todo-app/"
54
+ # post.fields["key"] #=> "my-todo-app/avatars/heidi/400x400.png"
55
+ # post.fields["policy"] #=> "ABC...XYZ"
56
+ # post.fields["x-goog-algorithm"] #=> "GOOG4-RSA-SHA256"
57
+ # post.fields["x-goog-credential"] #=> "cred@pid.iam.gserviceaccount.com/20200123/auto/storage/goog4_request"
58
+ # post.fields["x-goog-date"] #=> "20200128T000000Z"
59
+ # post.fields["x-goog-signature"] #=> "4893a0e...cd82"
60
+ #
44
61
  class PostObject
45
62
  attr_reader :url, :fields
46
63
 
@@ -357,10 +357,11 @@ module Google
357
357
  logging_bucket: nil, logging_prefix: nil,
358
358
  website_main: nil, website_404: nil, versioning: nil,
359
359
  requester_pays: nil, user_project: nil
360
- new_bucket = Google::Apis::StorageV1::Bucket.new({
360
+ params = {
361
361
  name: bucket_name,
362
362
  location: location
363
- }.delete_if { |_, v| v.nil? })
363
+ }.delete_if { |_, v| v.nil? }
364
+ new_bucket = Google::Apis::StorageV1::Bucket.new(**params)
364
365
  storage_class = storage_class_for storage_class
365
366
  updater = Bucket::Updater.new(new_bucket).tap do |b|
366
367
  b.logging_bucket = logging_bucket unless logging_bucket.nil?
@@ -522,6 +523,19 @@ module Google
522
523
  # using the URL, but only when the file resource is missing the
523
524
  # corresponding values. (These values can be permanently set using
524
525
  # {File#content_disposition=} and {File#content_type=}.)
526
+ # @param [String] scheme The URL scheme. The default value is `HTTPS`.
527
+ # @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
528
+ # hostname, which adds the bucket into the host portion of the URI rather
529
+ # than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
530
+ # For V4 signing, this also sets the `host` header in the canonicalized
531
+ # extension headers to the virtual hosted-style host, unless that header is
532
+ # supplied via the `headers` param. The default value of `false` uses the
533
+ # form of `https://storage.googleapis.com/mybucket`.
534
+ # @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
535
+ # replaces the `storage.googleapis.com` host with the name of a `CNAME`
536
+ # bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
537
+ # Cloud Load Balancer which routes to a bucket you own, e.g.
538
+ # `my-load-balancer-domain.tld`.
525
539
  # @param [Symbol, String] version The version of the signed credential
526
540
  # to create. Must be one of `:v2` or `:v4`. The default value is
527
541
  # `:v2`.
@@ -591,28 +605,50 @@ module Google
591
605
  # # Send the `x-goog-resumable:start` header and the content type
592
606
  # # with the resumable upload POST request.
593
607
  #
594
- def signed_url bucket, path, method: nil, expires: nil,
595
- content_type: nil, content_md5: nil, headers: nil,
596
- issuer: nil, client_email: nil, signing_key: nil,
597
- private_key: nil, query: nil, version: nil
608
+ def signed_url bucket,
609
+ path,
610
+ method: "GET",
611
+ expires: nil,
612
+ content_type: nil,
613
+ content_md5: nil,
614
+ headers: nil,
615
+ issuer: nil,
616
+ client_email: nil,
617
+ signing_key: nil,
618
+ private_key: nil,
619
+ query: nil,
620
+ scheme: "HTTPS",
621
+ virtual_hosted_style: nil,
622
+ bucket_bound_hostname: nil,
623
+ version: nil
598
624
  version ||= :v2
599
625
  case version.to_sym
600
626
  when :v2
601
627
  signer = File::SignerV2.new bucket, path, service
602
628
 
603
- signer.signed_url method: method, expires: expires,
604
- headers: headers, content_type: content_type,
605
- content_md5: content_md5, issuer: issuer,
629
+ signer.signed_url method: method,
630
+ expires: expires,
631
+ headers: headers,
632
+ content_type: content_type,
633
+ content_md5: content_md5,
634
+ issuer: issuer,
606
635
  client_email: client_email,
607
636
  signing_key: signing_key,
608
- private_key: private_key, query: query
637
+ private_key: private_key,
638
+ query: query
609
639
  when :v4
610
640
  signer = File::SignerV4.new bucket, path, service
611
- signer.signed_url method: method, expires: expires,
612
- headers: headers, issuer: issuer,
641
+ signer.signed_url method: method,
642
+ expires: expires,
643
+ headers: headers,
644
+ issuer: issuer,
613
645
  client_email: client_email,
614
646
  signing_key: signing_key,
615
- private_key: private_key, query: query
647
+ private_key: private_key,
648
+ query: query,
649
+ scheme: scheme,
650
+ virtual_hosted_style: virtual_hosted_style,
651
+ bucket_bound_hostname: bucket_bound_hostname
616
652
  else
617
653
  raise ArgumentError, "version '#{version}' not supported"
618
654
  end
@@ -152,9 +152,8 @@ module Google
152
152
  ##
153
153
  # Creates a new bucket ACL.
154
154
  def insert_bucket_acl bucket_name, entity, role, user_project: nil
155
- new_acl = Google::Apis::StorageV1::BucketAccessControl.new(
156
- { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
157
- )
155
+ params = { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
156
+ new_acl = Google::Apis::StorageV1::BucketAccessControl.new(**params)
158
157
  execute do
159
158
  service.insert_bucket_access_control \
160
159
  bucket_name, new_acl, user_project: user_project(user_project)
@@ -182,9 +181,8 @@ module Google
182
181
  ##
183
182
  # Creates a new default ACL.
184
183
  def insert_default_acl bucket_name, entity, role, user_project: nil
185
- new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(
186
- { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
187
- )
184
+ param = { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
185
+ new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(**param)
188
186
  execute do
189
187
  service.insert_default_object_access_control \
190
188
  bucket_name, new_acl, user_project: user_project(user_project)
@@ -243,13 +241,13 @@ module Google
243
241
  def insert_notification bucket_name, topic_name, custom_attrs: nil,
244
242
  event_types: nil, prefix: nil, payload: nil,
245
243
  user_project: nil
246
- new_notification = Google::Apis::StorageV1::Notification.new(
244
+ params =
247
245
  { custom_attributes: custom_attrs,
248
246
  event_types: event_types(event_types),
249
247
  object_name_prefix: prefix,
250
248
  payload_format: payload_format(payload),
251
249
  topic: topic_path(topic_name) }.delete_if { |_k, v| v.nil? }
252
- )
250
+ new_notification = Google::Apis::StorageV1::Notification.new(**params)
253
251
 
254
252
  execute do
255
253
  service.insert_notification \
@@ -298,14 +296,14 @@ module Google
298
296
  storage_class: nil, key: nil, kms_key: nil,
299
297
  temporary_hold: nil, event_based_hold: nil,
300
298
  user_project: nil
301
- file_obj = Google::Apis::StorageV1::Object.new(
299
+ params =
302
300
  { cache_control: cache_control, content_type: content_type,
303
301
  content_disposition: content_disposition, md5_hash: md5,
304
302
  content_encoding: content_encoding, crc32c: crc32c,
305
303
  content_language: content_language, metadata: metadata,
306
304
  storage_class: storage_class, temporary_hold: temporary_hold,
307
305
  event_based_hold: event_based_hold }.delete_if { |_k, v| v.nil? }
308
- )
306
+ file_obj = Google::Apis::StorageV1::Object.new(**params)
309
307
  content_type ||= mime_type_for(path || Pathname(source).to_path)
310
308
 
311
309
  execute do
@@ -432,9 +430,8 @@ module Google
432
430
  # Creates a new file ACL.
433
431
  def insert_file_acl bucket_name, file_name, entity, role,
434
432
  generation: nil, user_project: nil
435
- new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(
436
- { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
437
- )
433
+ params = { entity: entity, role: role }.delete_if { |_k, v| v.nil? }
434
+ new_acl = Google::Apis::StorageV1::ObjectAccessControl.new(**params)
438
435
  execute do
439
436
  service.insert_object_access_control \
440
437
  bucket_name, file_name, new_acl,
@@ -16,7 +16,7 @@
16
16
  module Google
17
17
  module Cloud
18
18
  module Storage
19
- VERSION = "1.25.1".freeze
19
+ VERSION = "1.26.0".freeze
20
20
  end
21
21
  end
22
22
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: google-cloud-storage
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.25.1
4
+ version: 1.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Moore
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-01-06 00:00:00.000000000 Z
12
+ date: 2020-04-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: google-cloud-core