Cách tải lên và phân phát hình ảnh bằng Cloud Storage, Firestore và Cloud Run

Cách tải lên và phân phát hình ảnh bằng Cloud Storage, Firestore và Cloud Run

Thông tin về lớp học lập trình này

subjectLần cập nhật gần đây nhất: thg 4 4, 2025
account_circleTác giả: Một nhân viên của Google

1. Giới thiệu

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách tải lên và phân phát hình ảnh bằng Cloud Storage, Firestore và Cloud Run. Bạn cũng sẽ tìm hiểu cách sử dụng thư viện ứng dụng của Google để xác thực nhằm thực hiện lệnh gọi đến Gemini.

  • Cách triển khai ứng dụng FastAPI lên Cloud Run
  • Cách sử dụng thư viện ứng dụng của Google để xác thực
  • Cách tải tệp lên Cloud Storage bằng dịch vụ Cloud Run
  • Cách đọc và ghi dữ liệu vào Firestore
  • Cách truy xuất và hiển thị hình ảnh từ Cloud Storage trên dịch vụ Cloud Run

2. Cách thiết lập và các yêu cầu

Thiết lập các biến môi trường sẽ được sử dụng trong suốt lớp học lập trình này.

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

Bật API

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

Tạo một bộ chứa Cloud Storage để lưu trữ hình ảnh

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

Cho phép truy cập công khai vào bộ chứa mà bạn có thể tải lên và hiển thị hình ảnh trên trang web:

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

Tạo tài khoản dịch vụ bằng cách chạy lệnh sau:

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

Đồng thời cấp cho SA quyền truy cập vào Firestore và Bộ chứa 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. Tạo cơ sở dữ liệu Firestore

Chạy lệnh sau để tạo cơ sở dữ liệu Firestore

gcloud firestore databases create --location=nam5

4. Tạo ứng dụng

Tạo thư mục cho mã của bạn.

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

Trước tiên, bạn sẽ tạo các mẫu html bằng cách tạo thư mục mẫu.

mkdir templates
cd templates

Tạo một tệp mới có tên index.html với nội dung sau:

<!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>

Bây giờ, hãy tạo mã python và các tệp khác trong thư mục gốc

cd ..

Tạo tệp .gcloudignore có nội dung sau:

__pycache__

Tạo một tệp có tên main.py với nội dung sau:

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

Tạo Dockerfile có nội dung sau:

# 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"]

và tạo một pyproject.toml với nội dung sau

[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. Triển khai lên Cloud Run

Dưới đây là lệnh để triển khai lên Cloud Run. Mã của bạn được nén và gửi đến Cloud Build. Công cụ này sẽ sử dụng Dockerfile để tạo hình ảnh.

Vì đây là một bản triển khai dựa trên nguồn cho Cloud Run, nên trong Cloud Console cho dịch vụ này, bạn sẽ thấy thẻ Source (Nguồn) chứa mã của mình.

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. Thử nghiệm dịch vụ

Mở URL dịch vụ trong trình duyệt web và tải hình ảnh lên. Bạn sẽ thấy thông tin này xuất hiện trong danh sách.

7. Thay đổi quyền đối với bộ chứa công khai trong Cloud Storage

Như đã đề cập trước đó, lớp học lập trình này sử dụng một bộ chứa GCS công khai. Bạn nên xoá bộ chứa hoặc xoá quyền truy cập của allUsers vào bộ chứa bằng cách chạy lệnh sau:

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

Bạn có thể xác nhận quyền truy cập allUsers đã bị xoá bằng cách chạy lệnh sau:

gsutil iam get gs://$GCS_BUCKET_NAME

8. Xin chúc mừng

Chúc mừng bạn đã hoàn thành lớp học lập trình!

Nội dung đã đề cập

  • Cách triển khai ứng dụng FastAPI lên Cloud Run
  • Cách sử dụng thư viện ứng dụng của Google để xác thực
  • Cách tải tệp lên Cloud Storage bằng dịch vụ Cloud Run
  • Cách đọc và ghi dữ liệu vào Firestore
  • Cách truy xuất và hiển thị hình ảnh từ Cloud Storage trên dịch vụ Cloud Run

9. Dọn dẹp

Để xoá dịch vụ Cloud Run, hãy truy cập vào Cloud Console của Cloud Run tại https://console.cloud.google.com/run rồi xoá dịch vụ đó.

Để xoá bộ chứa trên Cloud Storage, bạn có thể chạy các lệnh sau:

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

Nếu chọn xoá toàn bộ dự án, bạn có thể truy cập vào https://console.cloud.google.com/cloud-resource-manager, chọn dự án bạn đã tạo ở Bước 2 rồi chọn Xoá. Nếu xoá dự án, bạn sẽ cần thay đổi dự án trong SDK trên đám mây. Bạn có thể xem danh sách tất cả dự án hiện có bằng cách chạy gcloud projects list.