Cara mengupload dan menayangkan gambar menggunakan Cloud Storage, Firestore, dan Cloud Run

Cara mengupload dan menayangkan gambar menggunakan Cloud Storage, Firestore, dan Cloud Run

Tentang codelab ini

subjectTerakhir diperbarui Apr 4, 2025
account_circleDitulis oleh Googler

1. Pengantar

Ringkasan

Dalam codelab ini, Anda akan mempelajari cara mengupload dan menayangkan gambar menggunakan Cloud Storage, Firestore, dan Cloud Run. Anda juga akan mempelajari cara menggunakan library klien Google untuk autentikasi guna melakukan panggilan ke Gemini.

Yang akan Anda pelajari

  • Cara men-deploy aplikasi FastAPI ke Cloud Run
  • Cara menggunakan library klien Google untuk autentikasi
  • Cara mengupload file ke Cloud Storage menggunakan layanan Cloud Run
  • Cara membaca dan menulis data ke Firestore
  • Cara mengambil dan menampilkan gambar dari Cloud Storage di layanan Cloud Run

2. Penyiapan dan Persyaratan

Tetapkan variabel lingkungan yang akan digunakan di seluruh codelab ini.

PROJECT_ID=dogfood-gcf-saraford
REGION
=us-central1
GCS_BUCKET_NAME
=dogfood-gcf-saraford-codelab-wietse-2

SERVICE_NAME
=fastapi-storage-firestore
SERVICE_ACCOUNT
=fastapi-storage-firestore-sa
SERVICE_ACCOUNT_ADDRESS
=$SERVICE_ACCOUNT@$PROJECT_ID.iam.gserviceaccount.com

Mengaktifkan API

gcloud services enable run.googleapis.com \
                       storage.googleapis.com \
                       firestore.googleapis.com \
                       cloudbuild.googleapis.com \
                       artifactregistry.googleapis.com

Membuat bucket Cloud Storage untuk menyimpan gambar

gsutil mb -p dogfood-gcf-saraford -l us-central1 gs://$GCS_BUCKET_NAME

Izinkan akses publik ke bucket tempat Anda dapat mengupload dan menampilkan gambar di situs:

gsutil iam ch allUsers:objectViewer gs://$GCS_BUCKET_NAME

Buat akun layanan dengan menjalankan perintah ini:

gcloud iam service-accounts create $SERVICE_ACCOUNT \
   
--display-name="SA for CR $SERVICE_ACCOUNT"

Dan berikan akses SA ke Firestore dan Bucket GCS

gcloud projects add-iam-policy-binding $PROJECT_ID \
   
--member="serviceAccount:$SERVICE_ACCOUNT_ADDRESS" \
   
--role="roles/datastore.user"

gsutil iam ch serviceAccount
:$SERVICE_ACCOUNT_ADDRESS:roles/storage.objectAdmin gs://$GCS_BUCKET_NAME

3. Membuat database Firestore

Jalankan perintah berikut untuk membuat database Firestore

gcloud firestore databases create --location=nam5

4. Membuat aplikasi

Buat direktori untuk kode Anda.

mkdir codelab-cr-fastapi-firestore-gcs
cd codelab
-cr-fastapi-firestore-gcs

Pertama, Anda akan membuat template html dengan membuat direktori template.

mkdir templates
cd templates

Buat file baru bernama index.html dengan konten berikut:

<!DOCTYPE html>
<html>
<head>
    <title>Cloud Run Image Upload Demo</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        .upload-form { margin-bottom: 20px; padding: 15px; border: 1px solid #ccc; border-radius: 5px; background-color: #f9f9f9; }
        .image-list { margin-top: 30px; }
        .image-item { border-bottom: 1px solid #eee; padding: 10px 0; }
        .image-item img { max-width: 100px; max-height: 100px; vertical-align: middle; margin-right: 10px;}
        .error { color: red; font-weight: bold; margin-top: 10px;}
    </style>
</head>
<body>

    <h1>Upload an Image</h1>
    <p>Files will be uploaded to GCS bucket: <strong>{{ bucket_name }}</strong> and metadata stored in Firestore.</p>

    <div class="upload-form">
        <form action="/upload" method="post" enctype="multipart/form-data">
            <input type="file" name="file" accept="image/*" required>
            <button type="submit">Upload Image</button>
        </form>
        {% if error_message %}
            <p class="error">{{ error_message }}</p>
        {% endif %}
    </div>

    <div class="image-list">
        <h2>Recently Uploaded Images:</h2>
        {% if images %}
            {% for image in images %}
            <div class="image-item">
                <a href="{{ image.gcs_url }}" target="_blank">
                   <img src="{{ image.gcs_url }}" alt="{{ image.filename }}" title="Click to view full size">
                </a>
                <span>{{ image.filename }}</span>
                <small>(Uploaded: {{ image.uploaded_at.strftime('%Y-%m-%d %H:%M:%S') if image.uploaded_at else 'N/A' }})</small><br/>
                <small><a href="{{ image.gcs_url }}" target="_blank">{{ image.gcs_url }}</a></small>
            </div>
            {% endfor %}
        {% else %}
            <p>No images uploaded yet or unable to retrieve list.</p>
        {% endif %}
    </div>

</body>
</html>

Sekarang buat kode python dan file lainnya di direktori root

cd ..

Buat file .gcloudignore dengan konten berikut:

__pycache__

Buat file bernama main.py dengan konten berikut:

import os
import datetime
from fastapi import FastAPI, File, UploadFile, Request, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from google.cloud import storage, firestore

# --- Configuration ---
# Get bucket name and firestore collection from Cloud Run env vars
GCS_BUCKET_NAME = os.environ.get("GCS_BUCKET_NAME", "YOUR_BUCKET_NAME_DEFAULT")
FIRESTORE_COLLECTION = os.environ.get("FIRESTORE_COLLECTION", "YOUR_FIRESTORE_DEFAULT")

# --- Initialize Google Client Libraries ---
# These client libraries will use the Application Default Credentials
# for your service account within the Cloud Run environment
storage_client = storage.Client()
firestore_client = firestore.Client()

# --- FastAPI App ---
app = FastAPI()
templates = Jinja2Templates(directory="templates")

# --- Routes ---
@app.get("/", response_class=HTMLResponse)
async def read_root(request: Request):
    """Serves the main upload form."""
   
   
# Query Firestore for existing images to display
   
images = []
   
try:
       
docs = firestore_client.collection(FIRESTORE_COLLECTION).order_by(
           
"uploaded_at", direction=firestore.Query.DESCENDING
       
).limit(10).stream() # Get latest 10 images
       
for doc in docs:
           
images.append(doc.to_dict())
   
except Exception as e:
       
print(f"Warning: Could not fetch images from Firestore: {e}")
       
# Continue without displaying images if Firestore query fails

   
return templates.TemplateResponse("index.html", {
       
"request": request,
       
"bucket_name": GCS_BUCKET_NAME,
       
"images": images # Pass images to the template
   
})

@app.post("/upload")
async def handle_upload(request: Request, file: UploadFile = File(...)):
    """Handles file upload, saves to GCS, and records in Firestore."""
   
if not file:
       
return {"message": "No upload file sent"}
   
elif not GCS_BUCKET_NAME or GCS_BUCKET_NAME == "YOUR_BUCKET_NAME_DEFAULT":
         
return {"message": "GCS Bucket Name not configured."}, 500 # Internal Server Error

   
try:
       
# 1. Upload to GCS
       
# note: to keep the demo code short, there are no file verifications
       
# for an actual real-world production app, you will want to add checks
       
gcs_url = upload_to_gcs(file, GCS_BUCKET_NAME)

       
# 2. Save metadata to Firestore
       
save_metadata_to_firestore(file.filename, gcs_url, FIRESTORE_COLLECTION)

       
# Redirect back to the main page after successful upload
       
return RedirectResponse(url="/", status_code=303) # Redirect using See Other

   
except Exception as e:
       
print(f"Upload failed: {e}")

       
return templates.TemplateResponse("index.html", {
           
"request": request,
           
"bucket_name": GCS_BUCKET_NAME,
           
"error_message": f"Upload failed: {e}",
           
"images": [] # Pass empty list on error or re-query
       
}, status_code=500)

# --- Helper Functions ---
def upload_to_gcs(uploadedFile: UploadFile, bucket_name: str) -> str:
    """Uploads a file to Google Cloud Storage and returns the public URL."""
   
try:
       
bucket = storage_client.bucket(bucket_name)

       
# Create a unique blob name (e.g., timestamp + original filename)
       
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d%H%M%S")
       
blob_name = f"{timestamp}_{uploadedFile.filename}"
       
blob = bucket.blob(blob_name)

       
# Upload the file
       
# Reset file pointer just in case
       
uploadedFile.file.seek(0)
       
blob.upload_from_file(uploadedFile.file, content_type=uploadedFile.content_type)

       
print(f"File {uploadedFile.filename} uploaded to gs://{bucket_name}/{blob_name}")
       
return blob.public_url # Return the public URL

   
except Exception as e:
       
print(f"Error uploading to GCS: {e}")
       
raise  # Re-raise the exception for FastAPI to handle

def save_metadata_to_firestore(filename: str, gcs_url: str, collection_name: str):
    """Saves image metadata to Firestore."""
   
try:
       
doc_ref = firestore_client.collection(collection_name).document()
       
doc_ref.set({
           
'filename': filename,
           
'gcs_url': gcs_url,
           
'uploaded_at': firestore.SERVER_TIMESTAMP # Use server timestamp
       
})
       
print(f"Metadata saved to Firestore collection {collection_name}")
   
except Exception as e:
       
print(f"Error saving metadata to Firestore: {e}")
       
# Consider raising the exception or handling it appropriately
       
raise # Re-raise the exception

Buat Dockerfile dengan konten berikut:

# Build stage
FROM python:3.12-slim AS builder

WORKDIR /app

# Install poetry
RUN pip install poetry
RUN poetry self add poetry-plugin-export

# Copy poetry files
COPY pyproject.toml poetry.lock* ./

# Copy application code
COPY . .

# Export dependencies to requirements.txt
RUN poetry export -f requirements.txt --output requirements.txt

# Final stage
FROM python:3.12-slim

WORKDIR /app

# Copy files from builder
COPY --from=builder /app/ .

# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# Compile bytecode to improve startup latency
# -q: Quiet mode
# -b: Write legacy bytecode files (.pyc) alongside source
# -f: Force rebuild even if timestamps are up-to-date
RUN python -m compileall -q -b -f .

# Expose port
EXPOSE 8080

# Run the application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8080"]

dan membuat pyproject.toml dengan hal berikut

[tool.poetry]
name = "cloud-run-fastapi-demo"
version = "0.1.0"
description = "Demo FastAPI app for Cloud Run showing GCS upload and Firestore integration."
authors = ["Your Name <you@example.com>"]
readme = "README.md"

[tool.poetry.dependencies]
python = "^3.12"
fastapi = "^0.110.0"
uvicorn = {extras = ["standard"], version = "^0.29.0"} # Includes python-multipart
google-cloud-storage = "^2.16.0"
google-cloud-firestore = "^2.16.0"
jinja2 = "^3.1.3"
python-multipart = "^0.0.20"

[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

5. Men-deploy ke Cloud Run

Berikut adalah perintah untuk men-deploy ke Cloud Run. Kode Anda di-zip dan dikirim ke Cloud Build, yang menggunakan Dockerfile untuk membuat image.

Karena ini adalah deployment berbasis sumber ke Cloud Run, di Cloud Console untuk layanan tersebut, Anda akan melihat tab Source yang berisi kode Anda.

gcloud run deploy $SERVICE_NAME \
 --source . \
 --allow-unauthenticated \
 --service-account=$SERVICE_ACCOUNT_ADDRESS \
 --set-env-vars=GCS_BUCKET_NAME=$GCS_BUCKET_NAME \
 --set-env-vars=FIRESTORE_COLLECTION=$FIRESTORE_COLLECTION

6. Menguji layanan

Buka URL Layanan di browser web Anda dan upload gambar. Anda akan melihatnya ditampilkan dalam daftar.

7. Mengubah izin di bucket Cloud Storage publik

Seperti yang disebutkan sebelumnya, codelab ini menggunakan bucket GCS publik. Sebaiknya hapus bucket atau hapus akses allUsers ke bucket dengan menjalankan perintah berikut:

gsutil iam ch -d allUsers:objectViewer gs://$GCS_BUCKET_NAME

Anda dapat mengonfirmasi bahwa akses allUsers telah dihapus dengan menjalankan perintah ini:

gsutil iam get gs://$GCS_BUCKET_NAME

8. Selamat

Selamat, Anda telah menyelesaikan codelab.

Yang telah kita bahas

  • Cara men-deploy aplikasi FastAPI ke Cloud Run
  • Cara menggunakan library klien Google untuk autentikasi
  • Cara mengupload file ke Cloud Storage menggunakan layanan Cloud Run
  • Cara membaca dan menulis data ke Firestore
  • Cara mengambil dan menampilkan gambar dari Cloud Storage di layanan Cloud Run

9. Pembersihan

Untuk menghapus layanan Cloud Run, buka Konsol Cloud Cloud Run di https://console.cloud.google.com/run, lalu hapus layanan tersebut.

Untuk menghapus bucket Cloud Storage, Anda dapat menjalankan perintah berikut:

echo "Deleting objects in gs://$GCS_BUCKET_NAME..."
gsutil rm
-r gs://$GCS_BUCKET_NAME/*

echo
"Deleting bucket gs://$GCS_BUCKET_NAME..."
gsutil rb gs
://$GCS_BUCKET_NAME

Jika memilih untuk menghapus seluruh project, Anda dapat membuka https://console.cloud.google.com/cloud-resource-manager, memilih project yang Anda buat di Langkah 2, lalu memilih Hapus. Jika menghapus project, Anda harus mengubah project di Cloud SDK. Anda dapat melihat daftar semua project yang tersedia dengan menjalankan gcloud projects list.