Upload an Excel Report to Google Cloud Storage
When reporting jobs run on Google Cloud — Cloud Run, Cloud Functions, Composer, a GKE CronJob — Cloud Storage is where their output naturally lands: durable, cheap, versionable, and readable by BigQuery, Looker Studio and anyone given access. The client library makes the upload itself one call. The decisions around it are what make a reporting bucket pleasant to live with: how objects are named so that runs do not overwrite each other, what metadata travels with the file, how a large upload is checked, how people without Google Cloud access get the file, and how old reports are cleaned up. This page, part of Publishing Excel Reports to Cloud Storage, goes through them with code that runs unchanged against a local emulator.
Prerequisites
pip install google-cloud-storage pandas openpyxl
# for local testing
docker run -d --rm -p 4443:4443 fsouza/fake-gcs-server -scheme http -port 4443
export STORAGE_EMULATOR_HOST=http://127.0.0.1:4443
The code on this page ran against fake-gcs-server, an open-source Cloud Storage emulator: with STORAGE_EMULATOR_HOST set, the client library talks to it instead of Google, and uses anonymous credentials. Unset the variable and the same code talks to the real service. The bucket acme-reports exists; weekly_sales.xlsx is this week's report and transactions_export.xlsx a larger file for the resumable upload.
Step 1 — Authenticate without key files
The client finds credentials through Application Default Credentials: the service account attached to a Cloud Run service or VM, gcloud auth application-default login on a developer machine, or workload identity on GKE. No JSON key file needs to be downloaded, stored or rotated:
from google.cloud import storage
client = storage.Client(project="acme-analytics")
bucket = client.bucket("acme-reports")
print(type(client._credentials).__name__, [b.name for b in client.list_buckets()])
Against the emulator the credentials are anonymous; on Google Cloud they are the job's service account, which needs a role that allows creating objects in the bucket — roles/storage.objectCreator is enough to upload, plus objectViewer if the job also reads. Key files still work (GOOGLE_APPLICATION_CREDENTIALS=/path/key.json) but are the thing security teams most want to eliminate; see Store Credentials for Excel Report Scripts Safely.
Step 2 — Name objects by report and date
Cloud Storage has no folders — only object names with slashes in them, which the console displays as folders. Put the report name and the reporting date in the name, so every run gets its own object and listing a prefix gives the history:
from datetime import date
def object_name(report, run_date, filename):
return f"{report}/{run_date:%Y-%m-%d}/{filename}"
name = object_name("sales", date(2026, 9, 28), "weekly_sales.xlsx")
print(name)
Dated names make reruns and backfills safe — a rerun for last Monday writes to last Monday's name — and make "the file from four weeks ago" a matter of listing. A fixed name that is overwritten every week is simpler for people who bookmark it; if you need both, write the dated object and also copy it to sales/latest/weekly_sales.xlsx.
Step 3 — Upload with content type, metadata and a precondition
Set the content type explicitly, attach metadata describing where the file came from, and add a precondition so an accidental second run cannot overwrite a report that was already published:
XLSX = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
def publish(bucket, local_path, name, metadata, overwrite=False):
blob = bucket.blob(name)
blob.metadata = metadata
blob.upload_from_filename(local_path, content_type=XLSX, checksum="md5",
if_generation_match=None if overwrite else 0)
return bucket.get_blob(name)
meta = {"report": "weekly-sales", "source": "erp-export", "rows": "4"}
blob = publish(bucket, "weekly_sales.xlsx", name, meta)
print(blob.name, blob.size, blob.content_type)
print(blob.metadata, "md5", blob.md5_hash)
if_generation_match=0 means "only if no object with this name exists"; a second attempt fails with a 412 Precondition Failed instead of silently replacing the published file. checksum="md5" makes the library compare the checksum the service computed with the local file and raise if they differ, so a corrupted upload never passes as success. The explicit content type means browsers and download tools open the file as a workbook rather than guessing from the name.
from google.api_core.exceptions import PreconditionFailed
try:
publish(bucket, "weekly_sales.xlsx", name, meta)
except PreconditionFailed as err:
print("already published:", err.code)
blob = publish(bucket, "weekly_sales.xlsx", name, meta, overwrite=True)
print("replaced deliberately, generation", blob.generation)
A deliberate rerun passes overwrite=True. With object versioning enabled on the bucket, the replaced version is kept as a noncurrent version, so even deliberate overwrites can be undone.
Step 4 — Large files: resumable, checksummed uploads
Files above a few megabytes are uploaded in chunks through a resumable upload, which can pick up where it left off after a network error. The library does this automatically for large files; set chunk_size to control it — it must be a multiple of 256 KB:
big = bucket.blob(object_name("transactions", date(2026, 9, 28), "transactions_export.xlsx"))
big.chunk_size = 1024 * 1024
big.upload_from_filename("transactions_export.xlsx", content_type=XLSX, checksum="md5", if_generation_match=0)
stored = bucket.get_blob(big.name)
print(stored.name, f"{stored.size:,} bytes")
The 3 MB file went up in three 1 MB chunks and was checksummed as a whole once the last chunk arrived. Smaller chunks mean less to resend after a failure and more requests overall; 8–16 MB suits large exports on a reliable network.
Step 5 — Give people a time-limited link
Colleagues without Google Cloud access can download a report through a signed URL: a link that carries a signature granting read access to one object until it expires. Signing needs a service-account identity, so it does not work with anonymous emulator credentials — the code is shown as it runs in production:
from datetime import timedelta
url = bucket.blob(name).generate_signed_url(version="v4", expiration=timedelta(days=7), method="GET",
response_disposition='attachment; filename="weekly_sales.xlsx"')
print(url)
Version 4 signed URLs can be valid for at most seven days. On Cloud Run or Compute Engine, where the credentials have no private key, pass service_account_email and an access_token so the library signs through the IAM API instead. response_disposition makes the browser download the file with a clean name instead of showing the long object path. Emailing the link rather than the file keeps large reports out of mailboxes; Send an Excel Report with Gmail covers sending it.
Step 6 — Delete old reports automatically
A reporting bucket grows forever unless something deletes old objects. A lifecycle rule on the bucket does it without any code running: delete objects older than a set age, or only those under a prefix:
bucket = client.get_bucket("acme-reports")
bucket.add_lifecycle_delete_rule(age=400, matches_prefix=["sales/"])
bucket.patch()
print(list(bucket.lifecycle_rules))
Four hundred days keeps a year of weekly reports plus a margin for year-on-year comparisons. The emulator used for this page accepts the call but does not store lifecycle rules, so this step was not exercised locally — check the bucket's lifecycle configuration in the console after applying it.
Performance notes
A weekly workbook of a few megabytes uploads in about a second from inside Google Cloud; most of the time is TLS setup and authentication, so reuse one client for every upload in a run rather than creating one per file. For many small files — one workbook per region or per customer — upload them in parallel from a small pool of workers, giving each worker its own client. Uploads from outside Google Cloud are limited by the local connection, and the resumable protocol is what keeps a slow or flaky link from restarting a large file from zero.
Common pitfalls
- Downloading service-account keys. Use Application Default Credentials; keys are a liability.
- One fixed object name for every run. Reruns overwrite history; name objects by report and date.
- No content type. Browsers may treat the file as generic binary; set the
.xlsxtype explicitly. - Silent overwrites. Use
if_generation_match=0for publish-once objects. - Signed URLs from credentials without a key. Sign through IAM with
service_account_emailandaccess_tokenon Cloud Run and GCE.
Conclusion
Let Application Default Credentials authenticate the job, name each object by report and date, and upload with the .xlsx content type, descriptive metadata, checksum="md5" and if_generation_match=0 so published reports are never overwritten by accident. Large files go up as resumable uploads in chunk multiples of 256 KB, people outside Google Cloud get a signed URL valid for up to seven days, and a lifecycle rule removes old reports. Test all of it locally with fake-gcs-server by setting STORAGE_EMULATOR_HOST.
Frequently asked questions
How do I upload an Excel file to Google Cloud Storage with Python?
Install google-cloud-storage, create storage.Client(), get the bucket, and call upload_from_filename on a blob with the path and the xlsx MIME type as content_type. Credentials come from Application Default Credentials.
How do I stop an upload from overwriting an existing file? Pass if_generation_match=0. The upload then succeeds only if no object with that name exists, and otherwise fails with 412 Precondition Failed.
How can I share a Cloud Storage file with someone without Google access? Generate a V4 signed URL for the object with an expiration of up to seven days. Anyone with the link can download the file until it expires.
How do I test Cloud Storage code locally? Run the fake-gcs-server emulator in Docker and set STORAGE_EMULATOR_HOST to its address. The client library then sends requests to the emulator with anonymous credentials.
Related
- Up one level: Publishing Excel Reports to Cloud Storage — every destination for finished reports.
- Upload an Excel Report to Amazon S3 with boto3 — the AWS equivalent.
- Run an Excel Report on Google Cloud Run Jobs — where GCS uploads usually run.
- Upload an Excel Report to Azure Blob Storage — the Azure equivalent.
More on publishing & sharing files
- How-toRead an Excel File from SharePoint or OneDrive into pandas
- How-toShare an Excel File in OneDrive with a Link Using Graph
- How-toEmail a Download Link Instead of an Excel Attachment
- How-toSend an Excel Report with Amazon SES
- How-toGenerate an HTML Report from Excel Data with Jinja2
- How-toName, Version and Archive Generated Excel Reports