Pic-a-day: Lab 2—تصاویر کوچک تصاویر را ایجاد کنید

Pic-a-day:
Lab 2—تصاویر کوچک تصاویر را ایجاد کنید

درباره این codelab

subjectآخرین به‌روزرسانی: نوامبر ۱۴, ۲۰۲۱
account_circleنویسنده: Guillaume Laforge, Mete Atamel

1. نمای کلی

در این آزمایشگاه کد، شما بر روی آزمایشگاه قبلی ساخته شده و یک سرویس بند انگشتی اضافه می کنید. سرویس بندانگشتی یک محفظه وب است که تصاویر بزرگ می گیرد و تصاویر کوچک را از آنها ایجاد می کند.

همانطور که تصویر در Cloud Storage آپلود می شود، یک اعلان از طریق Cloud Pub/Sub به یک محفظه وب Cloud Run ارسال می شود، که سپس اندازه تصاویر را تغییر می دهد و آنها را در سطل دیگری در Cloud Storage ذخیره می کند.

31fa4f8a294d90df.png

چیزی که یاد خواهید گرفت

  • Cloud Run
  • فضای ذخیره سازی ابری
  • Cloud Pub/Sub

2. راه اندازی و الزامات

تنظیم محیط خود به خود

  1. به Google Cloud Console وارد شوید و یک پروژه جدید ایجاد کنید یا از یک موجود استفاده مجدد کنید. اگر قبلاً یک حساب Gmail یا Google Workspace ندارید، باید یک حساب ایجاد کنید .

96a9c957bc475304.png

b9a10ebdf5b5a448.png

a1e3c01a38fa61c2.png

  • نام پروژه نام نمایشی برای شرکت کنندگان این پروژه است. این یک رشته کاراکتری است که توسط API های Google استفاده نمی شود و می توانید هر زمان که بخواهید آن را به روز کنید.
  • شناسه پروژه باید در تمام پروژه‌های Google Cloud منحصربه‌فرد باشد و تغییرناپذیر باشد (پس از تنظیم نمی‌توان آن را تغییر داد). Cloud Console به طور خودکار یک رشته منحصر به فرد تولید می کند. معمولاً برای شما مهم نیست که چیست. در اکثر کدها، باید به شناسه پروژه ارجاع دهید (و معمولاً به عنوان PROJECT_ID شناخته می‌شود)، بنابراین اگر آن را دوست ندارید، یک نمونه تصادفی دیگر ایجاد کنید، یا می‌توانید شناسه پروژه را امتحان کنید و ببینید در دسترس است. سپس پس از ایجاد پروژه "یخ زده" می شود.
  • یک مقدار سوم وجود دارد، یک شماره پروژه که برخی از API ها از آن استفاده می کنند. در مورد هر سه این مقادیر در مستندات بیشتر بیاموزید.
  1. در مرحله بعد، برای استفاده از منابع Cloud/APIها، باید صورتحساب را در کنسول Cloud فعال کنید . اجرا کردن از طریق این کد لبه نباید هزینه زیادی داشته باشد، اگر اصلاً باشد. برای اینکه منابع را خاموش کنید تا بیش از این آموزش متحمل صورتحساب نشوید، دستورالعمل‌های «پاک‌سازی» را که در انتهای Codelab یافت می‌شود دنبال کنید. کاربران جدید Google Cloud واجد شرایط برنامه آزمایشی رایگان 300 دلاری هستند.

Cloud Shell را راه اندازی کنید

در حالی که Google Cloud را می توان از راه دور از لپ تاپ شما کار کرد، در این کد لبه از Google Cloud Shell استفاده خواهید کرد، یک محیط خط فرمان که در Cloud اجرا می شود.

از کنسول GCP روی نماد Cloud Shell در نوار ابزار بالا سمت راست کلیک کنید:

bce75f34b2c53987.png

تهیه و اتصال به محیط فقط چند لحظه طول می کشد. وقتی تمام شد، باید چیزی شبیه به این را ببینید:

f6ef2b5f13479f3a.png

این ماشین مجازی با تمام ابزارهای توسعه که شما نیاز دارید بارگذاری شده است. این یک فهرست اصلی 5 گیگابایتی دائمی را ارائه می دهد و در Google Cloud اجرا می شود و عملکرد و احراز هویت شبکه را تا حد زیادی افزایش می دهد. تمام کارهای شما در این آزمایشگاه به سادگی با یک مرورگر قابل انجام است.

3. API ها را فعال کنید

در این آزمایشگاه، برای ساخت تصاویر کانتینر به Cloud Build و برای استقرار کانتینر به Cloud Run نیاز دارید.

هر دو API را از Cloud Shell فعال کنید:

gcloud services enable cloudbuild.googleapis.com \
  run.googleapis.com

برای اتمام موفقیت آمیز باید عملیات را مشاهده کنید:

Operation "operations/acf.5c5ef4f6-f734-455d-b2f0-ee70b5a17322" finished successfully.

4. یک سطل دیگر ایجاد کنید

شما تصاویر کوچک تصاویر آپلود شده را در سطل دیگری ذخیره خواهید کرد. بیایید از gsutil برای ایجاد سطل دوم استفاده کنیم.

در داخل Cloud Shell، یک متغیر برای نام سطل یکتا تنظیم کنید. Cloud Shell قبلاً GOOGLE_CLOUD_PROJECT را روی شناسه پروژه منحصر به فرد شما تنظیم کرده است. می توانید آن را به نام سطل اضافه کنید. سپس، یک سطل عمومی چند منطقه ای در اروپا با دسترسی سطح یکنواخت ایجاد کنید:

BUCKET_THUMBNAILS=thumbnails-$GOOGLE_CLOUD_PROJECT
gsutil mb -l EU gs://$BUCKET_THUMBNAILS
gsutil uniformbucketlevelaccess set on gs://$BUCKET_THUMBNAILS
gsutil iam ch allUsers:objectViewer gs://$BUCKET_THUMBNAILS

در پایان، شما باید یک سطل عمومی جدید داشته باشید:

8e75c8099938e972.png

5. کد را کلون کنید

کد را کلون کنید و به دایرکتوری حاوی سرویس بروید:

git clone https://github.com/GoogleCloudPlatform/serverless-photosharing-workshop
cd serverless-photosharing-workshop/services/thumbnails/nodejs

شما طرح بندی فایل زیر را برای سرویس خواهید داشت:

services
 |
 ├── thumbnails
      |
      ├── nodejs
           |
           ├── Dockerfile
           ├── index.js
           ├── package.json

در داخل پوشه thumbnails/nodejs ، 3 فایل دارید:

  • index.js حاوی کد Node.js است
  • package.json وابستگی های کتابخانه را تعریف می کند
  • Dockerfile تصویر ظرف را تعریف می کند

6. کد را کاوش کنید

برای کشف کد، می‌توانید از ویرایشگر متن داخلی با کلیک بر روی دکمه Open Editor در بالای پنجره Cloud Shell استفاده کنید:

3d145fe299dd8b3e.png

همچنین می‌توانید ویرایشگر را در یک پنجره مرورگر اختصاصی باز کنید تا صفحه نمایش املاک بیشتری داشته باشید.

وابستگی ها

فایل package.json وابستگی های کتابخانه مورد نیاز را تعریف می کند:

{
  "name": "thumbnail_service",
  "version": "0.0.1",
  "main": "index.js",
  "scripts": {
    "start": "node index.js"
  },
  "dependencies": {
    "bluebird": "^3.7.2",
    "express": "^4.17.1",
    "imagemagick": "^0.1.3",
    "@google-cloud/firestore": "^4.9.9",
    "@google-cloud/storage": "^5.8.3"
  }
}

کتابخانه Cloud Storage برای خواندن و ذخیره فایل های تصویری در Cloud Storage استفاده می شود. Firestore برای به روز رسانی متادیتای تصویر. Express یک چارچوب وب جاوا اسکریپت / Node است. ماژول body-parser برای تجزیه درخواست های دریافتی به راحتی استفاده می شود. Bluebird برای مدیریت وعده ها استفاده می شود و Imagemagick یک کتابخانه برای دستکاری تصاویر است.

Dockerfile

Dockerfile تصویر ظرف را برای برنامه تعریف می کند:

FROM node:14-slim

# installing Imagemagick
RUN set -ex; \
  apt-get -y update; \
  apt-get -y install imagemagick; \
  rm -rf /var/lib/apt/lists/*; \
  mkdir /tmp/original; \
  mkdir /tmp/thumbnail;

WORKDIR /picadaily/services/thumbnails
COPY package*.json ./
RUN npm install --production
COPY . .
CMD [ "npm", "start" ]

تصویر پایه Node 14 است و کتابخانه imagemagick برای دستکاری تصویر استفاده می شود. برخی دایرکتوری‌های موقت برای نگهداری فایل‌های تصویر اصلی و بندانگشتی ایجاد می‌شوند. سپس ماژول های NPM مورد نیاز کد ما قبل از شروع کد با npm start نصب می شوند.

index.js

بیایید کد را به صورت تکه تکه بررسی کنیم تا بهتر بفهمیم این برنامه چه کاری انجام می دهد.

const express = require('express');
const imageMagick = require('imagemagick');
const Promise = require("bluebird");
const path = require('path');
const {Storage} = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');

const app = express();
app.use(express.json());

ما ابتدا به وابستگی‌های مورد نیاز نیاز داریم و برنامه وب Express خود را ایجاد می‌کنیم، و همچنین نشان می‌دهیم که می‌خواهیم از تجزیه‌کننده بدن JSON استفاده کنیم، زیرا درخواست‌های دریافتی در واقع فقط بارهای JSON هستند که از طریق یک درخواست POST به برنامه ما ارسال می‌شوند.

app.post('/', async (req, res) => {
    try {
        // ...
    } catch (err) {
        console.log(`Error: creating the thumbnail: ${err}`);
        console.error(err);
        res.status(500).send(err);
    }
});

ما آن بارهای دریافتی را در URL پایه / دریافت می‌کنیم، و کد خود را با برخی مدیریت منطق خطا می‌پیچانیم تا با نگاه کردن به گزارش‌هایی که از Stackdriver Logging قابل مشاهده است، اطلاعات بهتری در مورد اینکه چرا ممکن است چیزی در کد ما خراب است داشته باشیم. رابط در کنسول وب Google Cloud.

const pubSubMessage = req.body;
console.log(`PubSub message: ${JSON.stringify(pubSubMessage)}`);

const fileEvent = JSON.parse(Buffer.from(pubSubMessage.message.data, 'base64').toString().trim());
console.log(`Received thumbnail request for file ${fileEvent.name} from bucket ${fileEvent.bucket}`);

در پلتفرم Cloud Run، پیام‌های Pub/Sub از طریق درخواست‌های HTTP POST، به‌عنوان بارهای JSON از فرم ارسال می‌شوند:

{
  "message": {
    "attributes": {
      "bucketId": "uploaded-pictures",
      "eventTime": "2020-02-27T09:22:43.255225Z",
      "eventType": "OBJECT_FINALIZE",
      "notificationConfig": "projects/_/buckets/uploaded-pictures/notificationConfigs/28",
      "objectGeneration": "1582795363255481",
      "objectId": "IMG_20200213_181159.jpg",
      "payloadFormat": "JSON_API_V1"
    },
    "data": "ewogICJraW5kIjogInN0b3JhZ2Ujb2JqZWN...FQUU9Igp9Cg==",
    "messageId": "1014308302773399",
    "message_id": "1014308302773399",
    "publishTime": "2020-02-27T09:22:43.973Z",
    "publish_time": "2020-02-27T09:22:43.973Z"
  },
  "subscription": "projects/serverless-picadaily/subscriptions/gcs-events-subscription"
}

اما آنچه در این سند JSON واقعاً جالب است، در واقع چیزی است که در ویژگی message.data وجود دارد، که فقط یک رشته است اما بار واقعی را در Base 64 رمزگذاری می کند. به همین دلیل است که کد ما در بالا محتوای Base 64 این ویژگی را رمزگشایی می کند. . این ویژگی data پس از رمزگشایی حاوی سند JSON دیگری است که جزئیات رویداد Cloud Storage را نشان می‌دهد، که در میان سایر ابرداده‌ها، نام فایل و نام سطل را نشان می‌دهد.

{
  "kind": "storage#object",
  "id": "uploaded-pictures/IMG_20200213_181159.jpg/1582795363255481",
  "selfLink": "https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg",
  "name": "IMG_20200213_181159.jpg",
  "bucket": "uploaded-pictures",
  "generation": "1582795363255481",
  "metageneration": "1",
  "contentType": "image/jpeg",
  "timeCreated": "2020-02-27T09:22:43.255Z",
  "updated": "2020-02-27T09:22:43.255Z",
  "storageClass": "STANDARD",
  "timeStorageClassUpdated": "2020-02-27T09:22:43.255Z",
  "size": "4944335",
  "md5Hash": "QzBIoPJBV2EvqB1EVk1riw==",
  "mediaLink": "https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/IMG_20200213_181159.jpg?generation=1582795363255481&alt=media",
  "crc32c": "hQ3uHg==",
  "etag": "CLmJhJu08ecCEAE="
}

ما به نام تصویر و سطل علاقه مند هستیم، زیرا کد ما قرار است آن تصویر را از سطل برای پردازش تصویر کوچک آن واکشی کند:

const bucket = storage.bucket(fileEvent.bucket);
const thumbBucket = storage.bucket(process.env.BUCKET_THUMBNAILS);

const originalFile = path.resolve('/tmp/original', fileEvent.name);
const thumbFile = path.resolve('/tmp/thumbnail', fileEvent.name);

await bucket.file(fileEvent.name).download({
    destination: originalFile
});
console.log(`Downloaded picture into ${originalFile}`);

ما در حال بازیابی نام سطل ذخیره خروجی از یک متغیر محیطی هستیم.

ما سطل مبدا را داریم که ایجاد فایل آن سرویس Cloud Run ما را راه‌اندازی کرد و سطل مقصد را داریم که تصویر حاصل را در آن ذخیره می‌کنیم. ما از path API داخلی برای انجام مدیریت فایل های محلی استفاده می کنیم، زیرا کتابخانه imagemagick تصویر کوچک را به صورت محلی در دایرکتوری موقت /tmp ایجاد می کند. برای دانلود فایل تصویری آپلود شده await تماس ناهمزمان هستیم.

const resizeCrop = Promise.promisify(im.crop);
await resizeCrop({
        srcPath: originalFile,
        dstPath: thumbFile,
        width: 400,
        height: 400        
});
console.log(`Created local thumbnail in ${thumbFile}`);

ماژول imagemagick خیلی async نیست / await دوستانه نیست، بنابراین ما آن را در یک وعده جاوا اسکریپت (ارائه شده توسط ماژول Bluebird) جمع بندی می کنیم. سپس تابع تغییر اندازه / برش ناهمزمان را که با پارامترهای فایل های مبدا و مقصد و همچنین ابعاد تصویر کوچکی که می خواهیم ایجاد کنیم، فراخوانی می کنیم.

await thumbBucket.upload(thumbFile);
console.log(`Uploaded thumbnail to Cloud Storage bucket ${process.env.BUCKET_THUMBNAILS}`);

هنگامی که فایل تصویر کوچک در Cloud Storage آپلود شد، ما همچنین ابرداده ها را در Cloud Firestore به روز می کنیم تا یک پرچم بولین اضافه کنیم که نشان می دهد تصویر کوچک این تصویر واقعاً ایجاد شده است:

const pictureStore = new Firestore().collection('pictures');
const doc = pictureStore.doc(fileEvent.name);
await doc.set({
    thumbnail: true
}, {merge: true});
console.log(`Updated Firestore about thumbnail creation for ${fileEvent.name}`);

res.status(204).send(`${fileEvent.name} processed`);

هنگامی که درخواست ما تمام شد، به درخواست HTTP POST پاسخ می دهیم که فایل به درستی پردازش شده است.

const PORT = process.env.PORT || 8080;

app.listen(PORT, () => {
    console.log(`Started thumbnail generator on port ${PORT}`);
});

در پایان فایل منبع، دستورالعمل‌هایی داریم که Express واقعاً برنامه وب ما را روی پورت پیش‌فرض 8080 راه‌اندازی کند.

7. به صورت محلی تست کنید

کد را به صورت محلی تست کنید تا مطمئن شوید قبل از استقرار در فضای ابری کار می کند.

در داخل پوشه thumbnails/nodejs ، وابستگی‌های npm را نصب کرده و سرور را راه‌اندازی کنید:

npm install; npm start

اگر همه چیز خوب پیش رفت، باید سرور را روی پورت 8080 راه اندازی کند:

Started thumbnail generator on port 8080

برای خروج از CTRL-C استفاده کنید.

8. تصویر ظرف را بسازید و منتشر کنید

Cloud Run کانتینرها را اجرا می کند اما ابتدا باید تصویر کانتینر را بسازید (تعریف شده در Dockerfile ). از Google Cloud Build می توان برای ساخت تصاویر کانتینر و سپس میزبانی در Google Container Registry استفاده کرد.

در داخل پوشه thumbnails/nodejs که Dockerfile در آن قرار دارد، دستور زیر را برای ساخت تصویر کانتینر صادر کنید:

gcloud builds submit --tag gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

پس از یک یا دو دقیقه، ساخت باید با موفقیت انجام شود:

b354b3a9a3631097.png

بخش "تاریخچه" ساخت ابر باید ساخت موفق را نیز نشان دهد:

df00f198dd2bf6bf.png

با کلیک بر روی شناسه ساخت برای مشاهده جزئیات، در تب "build artifacts" باید مشاهده کنید که تصویر ظرف در Cloud Registry (GCR) بارگذاری شده است:

a4577ce0744f73e2.png

در صورت تمایل، می‌توانید دوباره بررسی کنید که تصویر کانتینر به صورت محلی در Cloud Shell اجرا شود:

docker run -p 8080:8080 gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service

باید سرور را روی پورت 8080 در کانتینر راه اندازی کند:

Started thumbnail generator on port 8080

برای خروج از CTRL-C استفاده کنید.

9. در Cloud Run مستقر شوید

قبل از استقرار در Cloud Run، منطقه Cloud Run را روی یکی از مناطق پشتیبانی شده و پلتفرم برای managed تنظیم کنید:

gcloud config set run/region europe-west1
gcloud config set run/platform managed

می توانید بررسی کنید که پیکربندی تنظیم شده است:

gcloud config list

...
[run]
platform = managed
region = europe-west1

دستور زیر را برای استقرار تصویر کانتینر در Cloud Run اجرا کنید:

SERVICE_NAME=thumbnail-service
gcloud run deploy $SERVICE_NAME \
    --image gcr.io/$GOOGLE_CLOUD_PROJECT/thumbnail-service \
    --no-allow-unauthenticated \
    --update-env-vars BUCKET_THUMBNAILS=$BUCKET_THUMBNAILS

به پرچم --no-allow-unauthenticated توجه کنید. این باعث می‌شود سرویس Cloud Run یک سرویس داخلی باشد که فقط توسط حساب‌های سرویس خاص راه‌اندازی می‌شود.

اگر استقرار موفقیت آمیز باشد، باید خروجی زیر را ببینید:

c0f28e7d6de0024.png

اگر به رابط کاربری کنسول ابری بروید، همچنین باید ببینید که سرویس با موفقیت مستقر شده است:

9bfe48e3c8b597e5.png

10. رویدادهای Cloud Storage به Cloud Run از طریق Pub/Sub

این سرویس آماده است، اما همچنان باید رویدادهای Cloud Storage را در سرویس Cloud Run جدید ایجاد کنید. Cloud Storage می‌تواند رویدادهای ایجاد فایل را از طریق Cloud Pub/Sub ارسال کند، اما چند مرحله برای این کار وجود دارد.

یک موضوع Pub/Sub به عنوان خط لوله ارتباطی ایجاد کنید:

TOPIC_NAME=cloudstorage-cloudrun-topic
gcloud pubsub topics create $TOPIC_NAME

وقتی فایل‌ها در سطل ذخیره می‌شوند، اعلان‌های Pub/Sub ایجاد کنید:

BUCKET_PICTURES=uploaded-pictures-$GOOGLE_CLOUD_PROJECT
gsutil notification create -t $TOPIC_NAME -f json gs://$BUCKET_PICTURES

یک حساب سرویس برای اشتراک Pub/Sub ایجاد کنید که بعداً ایجاد خواهیم کرد:

SERVICE_ACCOUNT=$TOPIC_NAME-sa
gcloud iam service-accounts create $SERVICE_ACCOUNT \
     --display-name "Cloud Run Pub/Sub Invoker"

به حساب سرویس اجازه فراخوانی یک سرویس Cloud Run بدهید:

SERVICE_NAME=thumbnail-service
gcloud run services add-iam-policy-binding $SERVICE_NAME \
   --member=serviceAccount:$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com \
   --role=roles/run.invoker

اگر حساب سرویس Pub/Sub را در تاریخ 8 آوریل 2021 یا قبل از آن فعال کرده‌اید، نقش iam.serviceAccountTokenCreator را به حساب سرویس Pub/Sub اعطا کنید:

PROJECT_NUMBER=$(gcloud projects describe $GOOGLE_CLOUD_PROJECT --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $GOOGLE_CLOUD_PROJECT \
     --member=serviceAccount:service-$PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com \
     --role=roles/iam.serviceAccountTokenCreator

ممکن است چند دقیقه طول بکشد تا تغییرات IAM منتشر شود.

در نهایت، یک اشتراک Pub/Sub با حساب سرویس ایجاد کنید:

SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --format 'value(status.url)')
gcloud pubsub subscriptions create $TOPIC_NAME-subscription --topic $TOPIC_NAME \
   --push-endpoint=$SERVICE_URL \
   --push-auth-service-account=$SERVICE_ACCOUNT@$GOOGLE_CLOUD_PROJECT.iam.gserviceaccount.com

می توانید بررسی کنید که یک اشتراک ایجاد شده است. در کنسول به Pub/Sub بروید، موضوع gcs-events را انتخاب کنید و در پایین، باید اشتراک را ببینید:

e8ab86dccb8d890.png

11. سرویس را تست کنید

برای آزمایش اینکه آیا راه‌اندازی کار می‌کند، یک عکس جدید در سطل uploaded-pictures آپلود کنید و در سطل thumbnails بررسی کنید که عکس‌های تغییر اندازه جدید مطابق انتظار ظاهر شوند.

همچنین می‌توانید گزارش‌ها را دوباره بررسی کنید تا پیام‌های گزارش‌گیری ظاهر شوند، زیرا مراحل مختلف سرویس Cloud Run در حال انجام است:

42c025e2d7d6ca3a.png

12. تمیز کردن (اختیاری)

اگر قصد ندارید با دیگر آزمایشگاه‌های این سری ادامه دهید، می‌توانید منابع را پاکسازی کنید تا در هزینه‌ها صرفه‌جویی کنید و در کل شهروند ابری خوبی باشید. می توانید منابع را به صورت جداگانه به صورت زیر پاک کنید.

سطل را حذف کنید:

gsutil rb gs://$BUCKET_THUMBNAILS

سرویس را حذف کنید:

gcloud run services delete $SERVICE_NAME -q

موضوع Pub/Sub را حذف کنید:

gcloud pubsub topics delete $TOPIC_NAME

یا می توانید کل پروژه را حذف کنید:

gcloud projects delete $GOOGLE_CLOUD_PROJECT

13. تبریک می گویم!

اکنون همه چیز سر جای خود است:

  • یک اعلان در فضای ذخیره‌سازی ابری ایجاد کرد که هنگام آپلود یک عکس جدید، پیام‌های Pub/Sub را در مورد موضوعی ارسال می‌کند.
  • اتصالات و حساب های مورد نیاز IAM را تعریف کرد (برخلاف توابع ابری که در آن همه خودکار است، در اینجا به صورت دستی پیکربندی می شود).
  • اشتراکی ایجاد کردیم تا سرویس Cloud Run ما پیام‌های Pub/Sub را دریافت کند.
  • هر زمان که عکس جدیدی در سطل آپلود می شود، به لطف سرویس جدید Cloud Run اندازه عکس تغییر می کند.

آنچه را پوشش داده ایم

  • Cloud Run
  • فضای ذخیره سازی ابری
  • Cloud Pub/Sub

مراحل بعدی