Repository: googleapis/google-cloud-python

Issue:
support for predefined ACL to be set directly on the upload request
This is a follow-up to #651.

This request may be a bit too early, as it will only really matter when #1185 is fixed. Right now with `blob.acl.save_predefined` the object ACL can be set to a predefined value separately, like the _metadata_ can be with `blob.patch`. The idea is to set everything with the upload request, to not have separate request.



Discussion:
I've made a simple patch for common use cases, on top of [another patch](https://github.com/GoogleCloudPlatform/google-cloud-python/issues/1185#issuecomment-287584661). It does not check the name of the predefined ACL, nor does it [translate](https://github.com/GoogleCloudPlatform/google-cloud-python/blob/b035bf78d726ddccc821e8884e6a4eda48e93e56/storage/google/cloud/storage/acl.py#L179) from XML to JSON names.

```Diff
--- a/blob.py
+++ b/blob.py
@@ -407,7 +407,8 @@
 
     def _create_upload(
             self, client, file_obj=None, size=None, content_type=None,
-            chunk_size=None, strategy=None, extra_headers=None):
+            chunk_size=None, strategy=None, extra_headers=None,
+            predefined_acl=None):
         """Helper for upload methods.
 
         Creates a :class:`google.cloud.core.streaming.Upload` object to handle
@@ -482,6 +483,7 @@
             upload.strategy = RESUMABLE_UPLOAD
 
         url_builder = _UrlBuilder(
+            predefined_acl=predefined_acl,
             bucket_name=self.bucket.name,
             object_name=self.name)
         upload_config = _UploadConfig()
@@ -497,6 +499,9 @@
 
         if self._properties:
             headers['content-type'] = 'application/json'
+            # 409 Cannot provide both a predefinedAcl and access controls
+            if predefined_acl and 'acl' in self._properties:
+                del self._properties['acl']
             request.body = json.dumps(self._properties)
 
         upload.configure_request(upload_config, request, url_builder)
@@ -525,7 +530,8 @@
                                  error_info=request.url)
 
     def upload_from_file(self, file_obj, rewind=False, size=None,
-                         content_type=None, num_retries=6, client=None):
+                         content_type=None, num_retries=6, client=None,
+                         predefined_acl=None):
         """Upload the contents of this blob from a file-like object.
 
         The content type of the upload will either be
@@ -621,7 +627,7 @@
         upload, request, _ = self._create_upload(
             client, file_obj=file_obj, size=total_bytes,
             content_type=content_type, chunk_size=chunk_size,
-            strategy=strategy)
+            strategy=strategy, predefined_acl=predefined_acl)
 
         if upload.strategy == RESUMABLE_UPLOAD:
             http_response = upload.stream_file(use_chunks=True)
@@ -637,7 +643,8 @@
             response_content = response_content.decode('utf-8')
         self._set_properties(json.loads(response_content))
 
-    def upload_from_filename(self, filename, content_type=None, client=None):
+    def upload_from_filename(self, filename, content_type=None, client=None,
+                             predefined_acl=None):
         """Upload this blob's contents from the content of a named file.
 
         The content type of the upload will either be
@@ -675,7 +682,8 @@
             self.upload_from_file(
                 file_obj, content_type=content_type, client=client)
 
-    def upload_from_string(self, data, content_type='text/plain', client=None):
+    def upload_from_string(self, data, content_type='text/plain', client=None,
+                           predefined_acl=None):
         """Upload contents of this blob from the provided string.
 
         .. note::
@@ -708,7 +716,8 @@
         string_buffer.write(data)
         self.upload_from_file(
             file_obj=string_buffer, rewind=True, size=len(data),
-            content_type=content_type, client=client)
+            content_type=content_type, client=client,
+            predefined_acl=predefined_acl)
 
     def upload_from_bytes(self, data, content_type=None, client=None):
 
@@ -1211,8 +1220,10 @@
 
 class _UrlBuilder(object):
     """Faux builder FBO apitools' 'configure_request'"""
-    def __init__(self, bucket_name, object_name):
+    def __init__(self, bucket_name, object_name, predefined_acl):
         self.query_params = {'name': object_name}
+        if predefined_acl:
+            self.query_params['predefinedAcl'] = predefined_acl
         self._bucket_name = bucket_name
         self._relative_path = ''
 
```
```Diff
--- a/blob.py
+++ b/blob.py
@@ -680,7 +680,8 @@
 
         with open(filename, 'rb') as file_obj:
             self.upload_from_file(
-                file_obj, content_type=content_type, client=client)
+                file_obj, content_type=content_type, client=client,
+                predefined_acl=predefined_acl)
 
     def upload_from_string(self, data, content_type='text/plain', client=None,
                            predefined_acl=None):

```
Hello, may I ask if you can make a pull request with this patch?

Or may I submit a PR on your behalf using the code above?
Sure, though I think you should perhaps add ```XML``` names to ```JSON``` names [translation](https://github.com/GoogleCloudPlatform/google-cloud-python/blob/b035bf78d726ddccc821e8884e6a4eda48e93e56/storage/google/cloud/storage/acl.py#L179).

Resolving patch:
diff --git a/storage/google/cloud/storage/acl.py b/storage/google/cloud/storage/acl.py
--- a/storage/google/cloud/storage/acl.py
+++ b/storage/google/cloud/storage/acl.py
@@ -208,6 +208,23 @@ def _ensure_loaded(self):
         if not self.loaded:
             self.reload()
 
+    @classmethod
+    def validate_predefined(cls, predefined):
+        """Ensures predefined is in list of predefined json values
+
+        :type predefined: str
+        :param predefined: name of a predefined acl
+
+        :type predefined: str
+        :param predefined: validated JSON name of predefined acl
+
+        :raises: :exc: `ValueError`: If predefined is not a valid acl
+        """
+        predefined = cls.PREDEFINED_XML_ACLS.get(predefined, predefined)
+        if predefined and predefined not in cls.PREDEFINED_JSON_ACLS:
+            raise ValueError("Invalid predefined ACL: %s" % (predefined,))
+        return predefined
+
     def reset(self):
         """Remove all entities from the ACL, and clear the ``loaded`` flag."""
         self.entities.clear()
@@ -502,11 +519,7 @@ def save_predefined(self, predefined, client=None):
         :param client: Optional. The client to use.  If not passed, falls back
                        to the ``client`` stored on the ACL's parent.
         """
-        predefined = self.PREDEFINED_XML_ACLS.get(predefined, predefined)
-
-        if predefined not in self.PREDEFINED_JSON_ACLS:
-            raise ValueError("Invalid predefined ACL: %s" % (predefined,))
-
+        predefined = self.validate_predefined(predefined)
         self._save(None, predefined, client)
 
     def clear(self, client=None):
diff --git a/storage/google/cloud/storage/blob.py b/storage/google/cloud/storage/blob.py
--- a/storage/google/cloud/storage/blob.py
+++ b/storage/google/cloud/storage/blob.py
@@ -55,6 +55,7 @@
 from google.cloud.storage._helpers import _PropertyMixin
 from google.cloud.storage._helpers import _scalar_property
 from google.cloud.storage._signing import generate_signed_url
+from google.cloud.storage.acl import ACL
 from google.cloud.storage.acl import ObjectACL
 
 
@@ -633,7 +634,7 @@ def _get_upload_arguments(self, content_type):
         return headers, object_metadata, content_type
 
     def _do_multipart_upload(self, client, stream, content_type,
-                             size, num_retries):
+                             size, num_retries, predefined_acl):
         """Perform a multipart upload.
 
         Assumes ``chunk_size`` is :data:`None` on the current blob.
@@ -664,6 +665,9 @@ def _do_multipart_upload(self, client, stream, content_type,
         :param num_retries: Number of upload retries. (Deprecated: This
                             argument will be removed in a future release.)
 
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
+
         :rtype: :class:`~requests.Response`
         :returns: The "200 OK" response object returned after the multipart
                   upload request.
@@ -688,6 +692,8 @@ def _do_multipart_upload(self, client, stream, content_type,
 
         if self.user_project is not None:
             name_value_pairs.append(('userProject', self.user_project))
+        if predefined_acl is not None:
+            name_value_pairs.append(('predefinedAcl', predefined_acl))
 
         upload_url = _add_query_parameters(base_url, name_value_pairs)
         upload = MultipartUpload(upload_url, headers=headers)
@@ -702,8 +708,9 @@ def _do_multipart_upload(self, client, stream, content_type,
         return response
 
     def _initiate_resumable_upload(self, client, stream, content_type,
-                                   size, num_retries, extra_headers=None,
-                                   chunk_size=None):
+                                   size, num_retries,
+                                   predefined_acl=None,
+                                   extra_headers=None, chunk_size=None):
         """Initiate a resumable upload.
 
         The content type of the upload will be determined in order
@@ -728,6 +735,9 @@ def _initiate_resumable_upload(self, client, stream, content_type,
                      from ``stream``). If not provided, the upload will be
                      concluded once ``stream`` is exhausted (or :data:`None`).
 
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
+
         :type num_retries: int
         :param num_retries: Number of upload retries. (Deprecated: This
                             argument will be removed in a future release.)
@@ -766,6 +776,8 @@ def _initiate_resumable_upload(self, client, stream, content_type,
 
         if self.user_project is not None:
             name_value_pairs.append(('userProject', self.user_project))
+        if predefined_acl is not None:
+            name_value_pairs.append(('predefinedAcl', predefined_acl))
 
         upload_url = _add_query_parameters(base_url, name_value_pairs)
         upload = ResumableUpload(upload_url, chunk_size, headers=headers)
@@ -781,7 +793,7 @@ def _initiate_resumable_upload(self, client, stream, content_type,
         return upload, transport
 
     def _do_resumable_upload(self, client, stream, content_type,
-                             size, num_retries):
+                             size, num_retries, predefined_acl):
         """Perform a resumable upload.
 
         Assumes ``chunk_size`` is not :data:`None` on the current blob.
@@ -812,19 +824,24 @@ def _do_resumable_upload(self, client, stream, content_type,
         :param num_retries: Number of upload retries. (Deprecated: This
                             argument will be removed in a future release.)
 
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
+
         :rtype: :class:`~requests.Response`
         :returns: The "200 OK" response object returned after the final chunk
                   is uploaded.
         """
         upload, transport = self._initiate_resumable_upload(
-            client, stream, content_type, size, num_retries)
+            client, stream, content_type, size, num_retries,
+            predefined_acl=predefined_acl)
 
         while not upload.finished:
             response = upload.transmit_next_chunk(transport)
 
         return response
 
-    def _do_upload(self, client, stream, content_type, size, num_retries):
+    def _do_upload(self, client, stream, content_type,
+                   size, num_retries, predefined_acl):
         """Determine an upload strategy and then perform the upload.
 
         If the current blob has a ``chunk_size`` set, then a resumable upload
@@ -857,6 +874,9 @@ def _do_upload(self, client, stream, content_type, size, num_retries):
         :param num_retries: Number of upload retries. (Deprecated: This
                             argument will be removed in a future release.)
 
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
+
         :rtype: dict
         :returns: The parsed JSON from the "200 OK" response. This will be the
                   **only** response in the multipart case and it will be the
@@ -864,15 +884,18 @@ def _do_upload(self, client, stream, content_type, size, num_retries):
         """
         if self.chunk_size is None:
             response = self._do_multipart_upload(
-                client, stream, content_type, size, num_retries)
+                client, stream, content_type,
+                size, num_retries, predefined_acl)
         else:
             response = self._do_resumable_upload(
-                client, stream, content_type, size, num_retries)
+                client, stream, content_type, size,
+                num_retries, predefined_acl)
 
         return response.json()
 
     def upload_from_file(self, file_obj, rewind=False, size=None,
-                         content_type=None, num_retries=None, client=None):
+                         content_type=None, num_retries=None, client=None,
+                         predefined_acl=None):
         """Upload the contents of this blob from a file-like object.
 
         The content type of the upload will be determined in order
@@ -930,6 +953,9 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
         :param client: (Optional) The client to use.  If not passed, falls back
                        to the ``client`` stored on the blob's bucket.
 
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
+
         :raises: :class:`~google.cloud.exceptions.GoogleCloudError`
                  if the upload response returns an error status.
 
@@ -941,14 +967,18 @@ def upload_from_file(self, file_obj, rewind=False, size=None,
             warnings.warn(_NUM_RETRIES_MESSAGE, DeprecationWarning)
 
         _maybe_rewind(file_obj, rewind=rewind)
+        predefined_acl = ACL.validate_predefined(predefined_acl)
+
         try:
             created_json = self._do_upload(
-                client, file_obj, content_type, size, num_retries)
+                client, file_obj, content_type,
+                size, num_retries, predefined_acl)
             self._set_properties(created_json)
         except resumable_media.InvalidResponse as exc:
             _raise_from_invalid_response(exc)
 
-    def upload_from_filename(self, filename, content_type=None, client=None):
+    def upload_from_filename(self, filename, content_type=None, client=None,
+                             predefined_acl=None):
         """Upload this blob's contents from the content of a named file.
 
         The content type of the upload will be determined in order
@@ -982,6 +1012,9 @@ def upload_from_filename(self, filename, content_type=None, client=None):
         :type client: :class:`~google.cloud.storage.client.Client`
         :param client: (Optional) The client to use.  If not passed, falls back
                        to the ``client`` stored on the blob's bucket.
+
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
         """
         content_type = self._get_content_type(content_type, filename=filename)
 
@@ -989,9 +1022,10 @@ def upload_from_filename(self, filename, content_type=None, client=None):
             total_bytes = os.fstat(file_obj.fileno()).st_size
             self.upload_from_file(
                 file_obj, content_type=content_type, client=client,
-                size=total_bytes)
+                size=total_bytes, predefined_acl=predefined_acl)
 
-    def upload_from_string(self, data, content_type='text/plain', client=None):
+    def upload_from_string(self, data, content_type='text/plain', client=None,
+                           predefined_acl=None):
         """Upload contents of this blob from the provided string.
 
         .. note::
@@ -1020,12 +1054,16 @@ def upload_from_string(self, data, content_type='text/plain', client=None):
                       ``NoneType``
         :param client: Optional. The client to use.  If not passed, falls back
                        to the ``client`` stored on the blob's bucket.
+
+        :type predefined_acl: str
+        :param predefined_acl: (Optional) predefined access control list
         """
         data = _to_bytes(data, encoding='utf-8')
         string_buffer = BytesIO(data)
         self.upload_from_file(
             file_obj=string_buffer, size=len(data),
-            content_type=content_type, client=client)
+            content_type=content_type, client=client,
+            predefined_acl=predefined_acl)
 
     def create_resumable_upload_session(
             self,
@@ -1110,6 +1148,7 @@ def create_resumable_upload_session(
             # matters when **sending** bytes to an upload.
             upload, _ = self._initiate_resumable_upload(
                 client, dummy_stream, content_type, size, None,
+                predefined_acl=None,
                 extra_headers=extra_headers,
                 chunk_size=self._CHUNK_SIZE_MULTIPLE)
 


Test patch:
diff --git a/storage/tests/system.py b/storage/tests/system.py
--- a/storage/tests/system.py
+++ b/storage/tests/system.py
@@ -408,6 +408,29 @@ def test_blob_acl_w_user_project(self):
         acl.save()
         self.assertFalse(acl.has_entity('allUsers'))
 
+    def test_upload_blob_acl(self):
+        control = self.bucket.blob('logo')
+        control_data = self.FILES['logo']        
+
+        blob = self.bucket.blob('SmallFile')
+        file_data = self.FILES['simple']
+
+        try:
+            control.upload_from_filename(control_data['path'])            
+            blob.upload_from_filename(file_data['path'],
+                                      predefined_acl='publicRead')
+        finally:
+            self.case_blobs_to_delete.append(blob)
+            self.case_blobs_to_delete.append(control)            
+
+        control_acl = control.acl
+        self.assertNotIn('READER', control_acl.all().get_roles())
+        acl = blob.acl
+        self.assertIn('READER', acl.all().get_roles())
+        acl.all().revoke_read()
+        self.assertSequenceEqual(acl.all().get_roles(), set([]))
+        self.assertEqual(control_acl.all().get_roles(), acl.all().get_roles())
+
     def test_write_metadata(self):
         filename = self.FILES['logo']['path']
         blob_name = os.path.basename(filename)
diff --git a/storage/tests/unit/test_acl.py b/storage/tests/unit/test_acl.py
--- a/storage/tests/unit/test_acl.py
+++ b/storage/tests/unit/test_acl.py
@@ -137,6 +137,14 @@ def _get_target_class():
     def _make_one(self, *args, **kw):
         return self._get_target_class()(*args, **kw)
 
+    def test_validate_predefined(self):
+        ACL = self._get_target_class()
+        self.assertIsNone(ACL.validate_predefined(None))
+        self.assertEqual(ACL.validate_predefined('public-read'), 'publicRead')
+        self.assertEqual(ACL.validate_predefined('publicRead'), 'publicRead')
+        with self.assertRaises(ValueError):
+            ACL.validate_predefined('publicread')
+
     def test_ctor(self):
         acl = self._make_one()
         self.assertEqual(acl.entities, {})
diff --git a/storage/tests/unit/test_blob.py b/storage/tests/unit/test_blob.py
--- a/storage/tests/unit/test_blob.py
+++ b/storage/tests/unit/test_blob.py
@@ -926,7 +926,8 @@ def _mock_transport(self, status_code, headers, content=b''):
         return fake_transport
 
     def _do_multipart_success(self, mock_get_boundary, size=None,
-                              num_retries=None, user_project=None):
+                              num_retries=None, user_project=None,
+                              predefined_acl=None):
         bucket = _Bucket(name='w00t', user_project=user_project)
         blob = self._make_one(u'blob-name', bucket=bucket)
         self.assertIsNone(blob.chunk_size)
@@ -940,7 +941,7 @@ def _do_multipart_success(self, mock_get_boundary, size=None,
         stream = io.BytesIO(data)
         content_type = u'application/xml'
         response = blob._do_multipart_upload(
-            client, stream, content_type, size, num_retries)
+            client, stream, content_type, size, num_retries, predefined_acl)
 
         # Check the mocks and the returned value.
         self.assertIs(response, transport.request.return_value)
@@ -959,6 +960,9 @@ def _do_multipart_success(self, mock_get_boundary, size=None,
             '/o?uploadType=multipart')
         if user_project is not None:
             upload_url += '&userProject={}'.format(user_project)
+        if predefined_acl is not None:
+            upload_url += '&predefinedAcl={}'.format(predefined_acl)
+
         payload = (
             b'--==0==\r\n' +
             b'content-type: application/json; charset=UTF-8\r\n\r\n' +
@@ -974,7 +978,7 @@ def _do_multipart_success(self, mock_get_boundary, size=None,
     @mock.patch(u'google.resumable_media._upload.get_boundary',
                 return_value=b'==0==')
     def test__do_multipart_upload_no_size(self, mock_get_boundary):
-        self._do_multipart_success(mock_get_boundary)
+        self._do_multipart_success(mock_get_boundary, predefined_acl='private')
 
     @mock.patch(u'google.resumable_media._upload.get_boundary',
                 return_value=b'==0==')
@@ -1002,7 +1006,7 @@ def test__do_multipart_upload_bad_size(self):
         self.assertGreater(size, len(data))
 
         with self.assertRaises(ValueError) as exc_info:
-            blob._do_multipart_upload(None, stream, None, size, None)
+            blob._do_multipart_upload(None, stream, None, size, None, None)
 
         exc_contents = str(exc_info.exception)
         self.assertIn(
@@ -1011,7 +1015,7 @@ def test__do_multipart_upload_bad_size(self):
 
     def _initiate_resumable_helper(
             self, size=None, extra_headers=None, chunk_size=None,
-            num_retries=None, user_project=None):
+            num_retries=None, user_project=None, predefined_acl=None):
         from google.resumable_media.requests import ResumableUpload
 
         bucket = _Bucket(name='whammy', user_project=user_project)
@@ -1038,7 +1042,8 @@ def _initiate_resumable_helper(
         content_type = u'text/plain'
         upload, transport = blob._initiate_resumable_upload(
             client, stream, content_type, size, num_retries,
-            extra_headers=extra_headers, chunk_size=chunk_size)
+            extra_headers=extra_headers,
+            chunk_size=chunk_size, predefined_acl=predefined_acl)
 
         # Check the returned values.
         self.assertIsInstance(upload, ResumableUpload)
@@ -1048,6 +1053,8 @@ def _initiate_resumable_helper(
             '/o?uploadType=resumable')
         if user_project is not None:
             upload_url += '&userProject={}'.format(user_project)
+        if predefined_acl is not None:
+            upload_url += '&predefinedAcl={}'.format(predefined_acl)
         self.assertEqual(upload.upload_url, upload_url)
         if extra_headers is None:
             self.assertEqual(upload._headers, {})
@@ -1114,6 +1121,9 @@ def test__initiate_resumable_upload_with_extra_headers(self):
     def test__initiate_resumable_upload_with_retry(self):
         self._initiate_resumable_helper(num_retries=11)
 
+    def test__initiate_resumable_upload_with_predefined_acl(self):
+        self._initiate_resumable_helper(predefined_acl='private')
+
     def _make_resumable_transport(self, headers1, headers2,
                                   headers3, total_bytes):
         from google import resumable_media
@@ -1134,12 +1144,14 @@ def _make_resumable_transport(self, headers1, headers2,
         return fake_transport, responses
 
     @staticmethod
-    def _do_resumable_upload_call0(blob, content_type, size=None):
+    def _do_resumable_upload_call0(blob, content_type, size=None, predefined_acl=None):
         # First mock transport.request() does initiates upload.
         upload_url = (
             'https://www.googleapis.com/upload/storage/v1' +
             blob.bucket.path +
             '/o?uploadType=resumable')
+        if predefined_acl is not None:
+            upload_url += '&predefinedAcl={}'.format(predefined_acl)
         expected_headers = {
             'content-type': 'application/json; charset=UTF-8',
             'x-upload-content-type': content_type,
@@ -1152,7 +1164,7 @@ def _do_resumable_upload_call0(blob, content_type, size=None):
 
     @staticmethod
     def _do_resumable_upload_call1(blob, content_type, data,
-                                   resumable_url, size=None):
+                                   resumable_url, size=None, predefined_acl=None):
         # Second mock transport.request() does sends first chunk.
         if size is None:
             content_range = 'bytes 0-{:d}/*'.format(blob.chunk_size - 1)
@@ -1170,7 +1182,7 @@ def _do_resumable_upload_call1(blob, content_type, data,
 
     @staticmethod
     def _do_resumable_upload_call2(blob, content_type, data,
-                                   resumable_url, total_bytes):
+                                   resumable_url, total_bytes, predefined_acl=None):
         # Third mock transport.request() does sends last chunk.
         content_range = 'bytes {:d}-{:d}/{:d}'.format(
             blob.chunk_size, total_bytes - 1, total_bytes)
@@ -1182,7 +1194,7 @@ def _do_resumable_upload_call2(blob, content_type, data,
         return mock.call(
             'PUT', resumable_url, data=payload, headers=expected_headers)
 
-    def _do_resumable_helper(self, use_size=False, num_retries=None):
+    def _do_resumable_helper(self, use_size=False, num_retries=None, predefined_acl=None):
         bucket = _Bucket(name='yesterday')
         blob = self._make_one(u'blob-name', bucket=bucket)
         blob.chunk_size = blob._CHUNK_SIZE_MULTIPLE
@@ -1208,18 +1220,18 @@ def _do_resumable_helper(self, use_size=False, num_retries=None):
         stream = io.BytesIO(data)
         content_type = u'text/html'
         response = blob._do_resumable_upload(
-            client, stream, content_type, size, num_retries)
+            client, stream, content_type, size, num_retries, predefined_acl)
 
         # Check the returned values.
         self.assertIs(response, responses[2])
         self.assertEqual(stream.tell(), total_bytes)
 
         # Check the mocks.
-        call0 = self._do_resumable_upload_call0(blob, content_type, size=size)
+        call0 = self._do_resumable_upload_call0(blob, content_type, size=size, predefined_acl=predefined_acl)
         call1 = self._do_resumable_upload_call1(
-            blob, content_type, data, resumable_url, size=size)
+            blob, content_type, data, resumable_url, size=size, predefined_acl=predefined_acl)
         call2 = self._do_resumable_upload_call2(
-            blob, content_type, data, resumable_url, total_bytes)
+            blob, content_type, data, resumable_url, total_bytes, predefined_acl=predefined_acl)
         self.assertEqual(
             transport.request.mock_calls, [call0, call1, call2])
 
@@ -1232,7 +1244,10 @@ def test__do_resumable_upload_with_size(self):
     def test__do_resumable_upload_with_retry(self):
         self._do_resumable_helper(num_retries=6)
 
-    def _do_upload_helper(self, chunk_size=None, num_retries=None):
+    def test__do_resumable_upload_with_predefined_acl(self):
+        self._do_resumable_helper(predefined_acl='private')
+
+    def _do_upload_helper(self, chunk_size=None, num_retries=None, predefined_acl=None):
         blob = self._make_one(u'blob-name', bucket=None)
 
         # Create a fake response.
@@ -1252,20 +1267,20 @@ def _do_upload_helper(self, chunk_size=None, num_retries=None):
         stream = mock.sentinel.stream
         content_type = u'video/mp4'
         size = 12345654321
-
         # Make the request and check the mocks.
         created_json = blob._do_upload(
-            client, stream, content_type, size, num_retries)
+            client, stream, content_type, size, num_retries, predefined_acl)
         self.assertIs(created_json, mock.sentinel.json)
         response.json.assert_called_once_with()
         if chunk_size is None:
+
             blob._do_multipart_upload.assert_called_once_with(
-                client, stream, content_type, size, num_retries)
+                client, stream, content_type, size, num_retries, predefined_acl)
             blob._do_resumable_upload.assert_not_called()
         else:
             blob._do_multipart_upload.assert_not_called()
             blob._do_resumable_upload.assert_called_once_with(
-                client, stream, content_type, size, num_retries)
+                client, stream, content_type, size, num_retries, predefined_acl)
 
     def test__do_upload_without_chunk_size(self):
         self._do_upload_helper()
@@ -1279,9 +1294,9 @@ def test__do_upload_with_retry(self):
 
     def _upload_from_file_helper(self, side_effect=None, **kwargs):
         from google.cloud._helpers import UTC
+        from google.cloud.storage.acl import ACL
 
         blob = self._make_one('blob-name', bucket=None)
-
         # Mock low-level upload helper on blob (it is tested elsewhere).
         created_json = {'updated': '2017-01-01T09:09:09.081Z'}
         blob._do_upload = mock.Mock(return_value=created_json, spec=[])
@@ -1295,6 +1310,7 @@ def _upload_from_file_helper(self, side_effect=None, **kwargs):
         stream.seek(2)  # Not at zero.
         content_type = u'font/woff'
         client = mock.sentinel.client
+        predefined_acl = kwargs.get('predefined_acl', None)
         ret_val = blob.upload_from_file(
             stream, size=len(data), content_type=content_type,
             client=client, **kwargs)
@@ -1308,12 +1324,12 @@ def _upload_from_file_helper(self, side_effect=None, **kwargs):
         # Check the mock.
         num_retries = kwargs.get('num_retries')
         blob._do_upload.assert_called_once_with(
-            client, stream, content_type, len(data), num_retries)
-
+            client, stream, content_type,
+            len(data), num_retries, predefined_acl)
         return stream
 
     def test_upload_from_file_success(self):
-        stream = self._upload_from_file_helper()
+        stream = self._upload_from_file_helper(predefined_acl='private')
         assert stream.tell() == 2
 
     @mock.patch('warnings.warn')
@@ -1353,11 +1369,12 @@ def _do_upload_mock_call_helper(self, blob, client, content_type, size):
         mock_call = blob._do_upload.mock_calls[0]
         call_name, pos_args, kwargs = mock_call
         self.assertEqual(call_name, '')
-        self.assertEqual(len(pos_args), 5)
+        self.assertEqual(len(pos_args), 6)
         self.assertEqual(pos_args[0], client)
         self.assertEqual(pos_args[2], content_type)
         self.assertEqual(pos_args[3], size)
         self.assertIsNone(pos_args[4])  # num_retries
+        self.assertIsNone(pos_args[5])  # predefined_acl
         self.assertEqual(kwargs, {})
 
         return pos_args[1]