كيفية تحميل الصور وعرضها باستخدام Cloud Storage وFirestore وCloud Run

1. مقدمة

نظرة عامة

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية تحميل الصور وعرضها باستخدام Cloud Storage وFirestore وCloud Run. ستتعرّف أيضًا على كيفية استخدام مكتبات عملاء Google للمصادقة لإجراء مكالمات إلى Gemini.

ما ستتعرّف عليه

  • كيفية نشر تطبيق FastAPI على Cloud Run
  • كيفية استخدام مكتبات برامج Google للمصادقة
  • كيفية تحميل ملف إلى Cloud Storage باستخدام خدمة Cloud Run
  • كيفية قراءة البيانات وكتابتها في Firestore
  • كيفية استرداد الصور وعرضها من Cloud Storage على خدمة Cloud Run

2. الإعداد والمتطلبات

اضبط متغيّرات البيئة التي سيتم استخدامها في هذا المختبر البرمجي.

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

تفعيل واجهات برمجة التطبيقات

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

أنشئ حزمة في Cloud Storage لتخزين الصور.

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

اسمح للجميع بالوصول إلى الحزمة التي يمكنك من خلالها تحميل الصور وعرضها على الموقع الإلكتروني:

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

أنشئ حساب خدمة من خلال تنفيذ هذا الأمر:

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

ومنح حساب "مشرف الحسابات" إذن الوصول إلى Firestore وحزمة 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- إنشاء قاعدة بيانات Firestore

نفِّذ الأمر التالي لإنشاء قاعدة بيانات Firestore.

gcloud firestore databases create --location=nam5

4. إنشاء التطبيق

أنشئ دليلاً لرمزك البرمجي.

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

أولاً، عليك إنشاء نماذج html من خلال إنشاء دليل للنماذج.

mkdir templates
cd templates

أنشئ ملفًا جديدًا باسم index.html يتضمّن المحتوى التالي:

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

الآن، أنشئ رمز Python والملفات الأخرى في الدليل الجذر.

cd ..

أنشئ ملف .gcloudignore يتضمّن المحتوى التالي:

__pycache__

أنشئ ملفًا باسم main.py يتضمّن المحتوى التالي:

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

أنشئ Dockerfile يتضمّن المحتوى التالي:

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

وأنشأت pyproject.toml باستخدام

[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- النشر على Cloud Run

في ما يلي الأمر المطلوب لنشر التطبيق على Cloud Run. يتم ضغط الرمز البرمجي وإرساله إلى Cloud Build الذي يستخدم Dockerfile لإنشاء الصورة.

بما أنّ هذا الإجراء هو عملية نشر مستندة إلى المصدر إلى Cloud Run، ستظهر لك علامة التبويب "المصدر" في Cloud Console للخدمة، والتي تحتوي على الرمز البرمجي.

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- اختبار الخدمة

افتح عنوان URL للخدمة في متصفّح الويب وحمِّل صورة. سيظهر لك في القائمة.

7- تغيير الأذونات في حزمة Cloud Storage العلنية

كما ذكرنا سابقًا، يستخدم هذا الدليل التعليمي مجموعة GCS علنية. ننصحك بحذف الحزمة أو إزالة إذن وصول allUsers إلى الحزمة من خلال تنفيذ الأمر التالي:

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

يمكنك التأكّد من إزالة إذن وصول allUsers من خلال تنفيذ هذا الأمر:

gsutil iam get gs://$GCS_BUCKET_NAME

8. تهانينا

تهانينا على إكمال دورة codelab.

المواضيع التي تناولناها

  • كيفية نشر تطبيق FastAPI على Cloud Run
  • كيفية استخدام مكتبات برامج Google للمصادقة
  • كيفية تحميل ملف إلى Cloud Storage باستخدام خدمة Cloud Run
  • كيفية قراءة البيانات وكتابتها في Firestore
  • كيفية استرداد الصور وعرضها من Cloud Storage على خدمة Cloud Run

9- تَنظيم

لحذف خدمة Cloud Run، انتقِل إلى وحدة تحكّم Cloud Run على الرابط https://console.cloud.google.com/run واحذِف الخدمة.

لحذف حزمة Cloud Storage، يمكنك تنفيذ الأوامر التالية:

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

إذا اخترت حذف المشروع بأكمله، يمكنك الانتقال إلى https://console.cloud.google.com/cloud-resource-manager واختيار المشروع الذي أنشأته في الخطوة 2 ثم اختيار "حذف". في حال حذف المشروع، عليك تغيير المشاريع في حزمة Cloud SDK. يمكنك عرض قائمة بجميع المشاريع المتاحة من خلال تشغيل gcloud projects list.