# Buggy code
def write_fh(
    self,
    fh,
    key: str,
    md5: bytes,
    metadata: Optional[Dict[str, Any]] = None,
    size: Optional[int] = None,
):
    """Write data from byte stream *fh* into *key*.

    *fh* must be seekable. If *size* is None, *fh* must also implement
    `fh.fileno()` so that the size can be determined through `os.fstat`.

    *md5* must be the (binary) md5 checksum of the data.
    """

    metadata = json.dumps(
        {
            "metadata": _wrap_user_meta(metadata if metadata else {}),
            "md5Hash": b64encode(md5).decode(),
            "name": self.prefix + key,
        }
    )

    # Google Storage uses Content-Length to read the object data, so we
    # don't have to worry about the boundary occurring in the object data.
    boundary = "foo_bar_baz"
    headers = CaseInsensitiveDict()
    headers["Content-Type"] = "multipart/related; boundary=%s" % boundary

    body_prefix = "\n".join(
        (
            "--" + boundary,
            "Content-Type: application/json; charset=UTF-8",
            "",
            metadata,
            "--" + boundary,
            "Content-Type: application/octet-stream",
            "",
            "",
        )
    ).encode()
    body_suffix = ("\n--%s--\n" % boundary).encode()

    body_size = len(body_prefix) + len(body_suffix)
    if size is not None:
        body_size += size
    else:
        body_size += os.fstat(fh.fileno()).st_size

    path = "/upload/storage/v1/b/%s/o" % (
        urllib.parse.quote(self.bucket_name, safe=""),
    )
    query_string = {"uploadType": "multipart"}
    try:
        resp = self._do_request(
            "POST",
            path,
            query_string=query_string,
            headers=headers,
            body=BodyFollowing(body_size),
        )
    except RequestError as exc:
        exc = _map_request_error(exc, key)
        if exc:
            raise exc
        raise

    assert resp.status == 100
    fh.seek(0)

    md5_run = hashlib.md5()
    try:
        self.conn.write(body_prefix)
        while True:
            buf = fh.read(BUFSIZE)
            if not buf:
                break
            self.conn.write(buf)
            md5_run.update(buf)
        self.conn.write(body_suffix)
    except ConnectionClosed:
        # Server closed connection while we were writing body data -
        # but we may still be able to read an error response
        try:
            resp = self.conn.read_response()
        except ConnectionClosed:  # No server response available
            pass
        else:
            if resp.status >= 400:  # Got error response
                return resp
                log.warning(
                    "Server broke connection during upload, but signaled %d %s",
                    resp.status,
                    resp.reason,
                )
        # Re-raise first ConnectionClosed exception
        raise

    if md5_run.digest() != md5:
        raise ValueError("md5 passed to write_fd does not match fd data")

    resp = self.conn.read_response()
    if resp.status != 200:
        exc = self._parse_error_response(resp)
        raise _map_request_error(exc, key) or exc
    self._parse_json_response(resp)


# Traceback (s3ql.backends.gs.RequestError)
2019-02-20 18:51:00.053 1467:MainThread s3ql.mount.determine_threads: Using 4 upload threads.
2019-02-20 18:51:00.054 1467:MainThread s3ql.mount.main: Autodetected 1048532 file descriptors available for cache entries
2019-02-20 18:51:00.247 1467:MainThread s3ql.backends.gs._get_access_token: Requesting new access token
2019-02-20 18:51:06.003 1467:MainThread s3ql.mount.get_metadata: Using cached metadata.
2019-02-20 18:51:06.007 1467:MainThread s3ql.mount.main: Setting cache size to 55240 MB
2019-02-20 18:51:06.008 1467:MainThread s3ql.mount.main: Mounting gs://bucket/main at /mnt/bucketmain...
2019-02-20 18:51:06.015 1473:MainThread s3ql.daemonize.detach_process_context: Daemonizing, new PID is 1474
2019-02-20 19:55:08.263 1474:Thread-6 root.excepthook: Uncaught top-level exception:
Traceback (most recent call last):
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/mount.py", line 58, in run_with_except_hook
run_old(*args, **kw)
File "/usr/lib/python3.6/threading.py", line 864, in run
self._target(*self._args, **self._kwargs)
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/block_cache.py", line 445, in _upload_loop
self._do_upload(*tmp)
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/block_cache.py", line 472, in _do_upload
% obj_id).get_obj_size()
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/common.py", line 108, in wrapped
return method(*a, **kw)
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/common.py", line 279, in perform_write
return fn(fh)
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/comprenc.py", line 389, in __exit__
self.close()
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/comprenc.py", line 383, in close
self.fh.close()
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/comprenc.py", line 548, in close
self.fh.close()
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/gs.py", line 933, in close
self.metadata, size=self.obj_size)
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/common.py", line 108, in wrapped
return method(*a, **kw)
File "/root/.virtualenvs/s3ql/lib/python3.6/site-packages/s3ql/backends/gs.py", line 485, in write_fh
raise _map_request_error(exc, key) or exc
s3ql.backends.gs.RequestError: <RequestError, code=401, reason='Unauthorized', message='Invalid Credentials'>
2019-02-20 19:55:08.321 1474:Thread-5 s3ql.mount.exchook: Unhandled top-level exception during shutdown (will not be re-raised)
2019-02-20 19:55:08.322 1474:Thread-5 root.excepthook: Uncaught top-level exception:

# Fixed code
def write_fh(
    self,
    fh,
    key: str,
    md5: bytes,
    metadata: Optional[Dict[str, Any]] = None,
    size: Optional[int] = None,
):
    """Write data from byte stream *fh* into *key*.

    *fh* must be seekable. If *size* is None, *fh* must also implement
    `fh.fileno()` so that the size can be determined through `os.fstat`.

    *md5* must be the (binary) md5 checksum of the data.
    """

    metadata = json.dumps(
        {
            "metadata": _wrap_user_meta(metadata if metadata else {}),
            "md5Hash": b64encode(md5).decode(),
            "name": self.prefix + key,
        }
    )

    # Google Storage uses Content-Length to read the object data, so we
    # don't have to worry about the boundary occurring in the object data.
    boundary = "foo_bar_baz"
    headers = CaseInsensitiveDict()
    headers["Content-Type"] = "multipart/related; boundary=%s" % boundary

    body_prefix = "\n".join(
        (
            "--" + boundary,
            "Content-Type: application/json; charset=UTF-8",
            "",
            metadata,
            "--" + boundary,
            "Content-Type: application/octet-stream",
            "",
            "",
        )
    ).encode()
    body_suffix = ("\n--%s--\n" % boundary).encode()

    body_size = len(body_prefix) + len(body_suffix)
    if size is not None:
        body_size += size
    else:
        body_size += os.fstat(fh.fileno()).st_size

    path = "/upload/storage/v1/b/%s/o" % (
        urllib.parse.quote(self.bucket_name, safe=""),
    )
    query_string = {"uploadType": "multipart"}
    try:
        resp = self._do_request(
            "POST",
            path,
            query_string=query_string,
            headers=headers,
            body=BodyFollowing(body_size),
        )
    except RequestError as exc:
        exc = _map_request_error(exc, key)
        if exc:
            raise exc
        raise

    assert resp.status == 100
    fh.seek(0)

    md5_run = hashlib.md5()
    try:
        self.conn.write(body_prefix)
        while True:
            buf = fh.read(BUFSIZE)
            if not buf:
                break
            self.conn.write(buf)
            md5_run.update(buf)
        self.conn.write(body_suffix)
    except ConnectionClosed:
        # Server closed connection while we were writing body data -
        # but we may still be able to read an error response
        try:
            resp = self.conn.read_response()
        except ConnectionClosed:  # No server response available
            pass
        else:
            if resp.status >= 400:  # Got error response
                return resp
                log.warning(
                    "Server broke connection during upload, but signaled %d %s",
                    resp.status,
                    resp.reason,
                )
        # Re-raise first ConnectionClosed exception
        raise

    if md5_run.digest() != md5:
        raise ValueError("md5 passed to write_fd does not match fd data")

    resp = self.conn.read_response()
    if resp.status != 200:
        exc = self._parse_error_response(resp)
        # If we're really unlucky, then the token has expired while we
        # were uploading data.
        if exc.message == "Invalid Credentials":
            raise AccessTokenExpired()
        raise _map_request_error(exc, key) or exc
    self._parse_json_response(resp)