پشتیبان و بازیابی بکاند؛ پروژهٔ نهایی و آمادگی برای کار واقعی
در این درس ۵ بخش
فرض کنید ماشین آزمایشی دیگر روشن نمیشود. داشتن چند فایل JavaScript روی لپتاپ، برای بازگرداندن همهٔ خدمت کافی نیست: باید بدانید Node کدام نسخه بوده، برنامه با چه حسابی اجرا میشده، متغیرهای محیطی کجا بودهاند، Nginx چه مسیرهایی را مجاز کرده و HTTPS با کدام دامنه و گواهی کار میکرده است. هدف این درس آن است که بتوانید از یک نوشته و نسخهٔ پشتیبان، سامانه را روی یک ماشین تازه بازسازی و نتیجه را مستقل تأیید کنید. «پشتیبان» یک نسخهٔ نگهداریشده برای بازیابی است؛ «بازیابی» فرایند بازگرداندن وضعیت قابل استفاده و آزمودن آن است. وجود فایل آرشیو سالم، هنوز به معنی موفقیت بازیابی نیست. پروژهٔ نهایی این دوره، تحویل یک بکاند آزمایشی همراه با شناسنامه، فرمانهای بررسی، نسخهٔ پشتیبان محافظتشده و گزارش یک تمرین بازیابی است. از سرویس واقعی سازمان یا دادهٔ شخصی برای این تمرین استفاده نکنید.
دقیقاً چه چیزهایی باید قابل بازیابی باشند؟
داراییها را به چند گروه بشناسید. کد برنامه و فایل package.json مشخص میکنند چه منطقی اجرا میشود. فایل واحد systemd مشخص میکند چه فرایندی با چه هویتی، محیط و سیاست شروع مجدد اجرا شود. فایل محیطی PORT و تنظیمات را نگه میدارد و در محصول واقعی ممکن است راز داشته باشد. پیکربندی Nginx مسیر درخواست، دامنه، محدودیت روشها و قالب لاگ را تعیین میکند. گواهی و کلید خصوصی TLS و اطلاعات حساب ACME نیز برای ادامهٔ HTTPS اهمیت دارند. دادهٔ تجاری، گروه جداگانهای است: در این تمرین فقط Map داخل حافظه داریم و هیچ پایگاه دادهٔ ماندگاری ساخته نشده است. کپی پوشهٔ releases، فهرست کارهای حافظهای را ذخیره نمیکند. اجرای JSON.stringify روی یک داده در مرورگر هم خودبهخود پشتیبان سرور نیست؛ #json قالب انتقال و ذخیرهٔ داده است و محل، زمان و سیاست نگهداری را تعیین نمیکند. در شناسنامه صریحاً بنویسید کدام داده با restart از بین میرود و چه چیزی تحت پوشش پشتیبان است.
دو پرسش عملی بپرسید: از دسترفتن چه مقدار تغییر قابل قبول است و بازگرداندن خدمت چقدر میتواند طول بکشد؟ اولی هدف نقطهٔ بازیابی و دومی هدف زمان بازیابی است. اگر هر شب پشتیبان بگیرید، معمولاً نمیتوانید بدون سازوکار دیگری قول حفظ همهٔ تغییرهای چند دقیقهٔ اخیر را بدهید. در آزمایشگاه این دوره، هدف نمونه این است که کد و پیکربندی آخرین انتشار ثبتشده را بازسازی کنیم؛ دادهٔ حافظهای در تعهد بازیابی نیست. برای یک فروشگاه واقعی، این هدف کافی نیست و باید پایگاه داده، سفارشهای تازه، فایلهای بارگذاریشده و سازگاری نسخهها نیز بررسی شوند. snapshot ماشین معمولاً وابسته به سرویسدهنده و زمان ثبت است؛ ممکن است برای بازگشت سریع مفید باشد، ولی بهتنهایی جای نسخهٔ مستقل و آزموده خارج از همان ماشین را نمیگیرد. هدفها را با عدد و محدودهٔ روشن بنویسید و سپس اندازه بگیرید تمرین واقعی چه زمانی طول کشیده است؛ تخمین خوشبینانه را بهعنوان نتیجهٔ آزمایش گزارش نکنید.
date -Is
uname -m
/opt/node-current/bin/node --version
readlink -f /opt/node-current
readlink -f /opt/darsavand-node/current
sudo systemctl show darsavand-node.service \
-p FragmentPath -p User -p Group -p WorkingDirectory
nginx -vخروجیها را در یک گزارش شخصی با نام دامنهٔ واقعی و زمان ثبت کنار هم بگذارید. فرمان systemctl show فقط ویژگیهای انتخابشده را نشان میدهد؛ همهٔ Environment یا محتوای app.env را برای مستندسازی چاپ نمیکنیم. نسخهٔ باینری Node و مقصد واقعی پیوند آن را ثبت کنید تا بعدها «همان نام مسیر» را با «همان نسخهٔ runtime» اشتباه نگیرید. همچنین یادداشت کنید دسترسی SSH از چه پورت واقعی و کدام روش کلید انجام میشود، اما کلید خصوصی، رمز یا کد بازیابی را در گزارش عمومی نگذارید. فایل پیکربندیِ صرف میتواند به چیزهای بیرونی وابسته باشد؛ مثلاً Nginx پس از اجرای Certbot به گواهیهای /etc/letsencrypt اشاره میکند. شناسنامه باید این وابستگی را نشان دهد. به همین دلیل انتقال فقط فایل sites-available به ماشینی که گواهی ندارد، ممکن است حتی از مرحلهٔ nginx -t عبور نکند. بازسازی از روی شناسنامه باید به خواننده بگوید کدام اجزا از پشتیبان میآیند و کدام باید دوباره نصب یا صادر شوند.
قدم دوم: پشتیبان را از همان ابتدا رمزگذاری کنید
آرشیوی که app.env یا کلید خصوصی TLS دارد، یک فایل حساس است. آن را به بخش عمومی سایت، مخزن Git یا تیکت عمومی نفرستید. در نمونهٔ بعدی، tar دادهها را به خروجی استاندارد میفرستد و GnuPG همان جریان را بهصورت رمزگذاریشده ذخیره میکند؛ فایل میانیِ بازِ حاوی راز نمیسازیم. GnuPG ابزار رمزگذاری است و برای روش متقارن، عبارت عبور قوی و مستقل میخواهد. این عبارت را در آرگومان خط فرمان یا اسکریپت ننویسید؛ آن را در پرسش تعاملی ابزار وارد و روش نگهداری جداگانهٔ آن را مشخص کنید. اگر فرمان gpg روی ماشین آزمایشی وجود ندارد، آن را از بستهٔ gnupg مخزن رسمی اوبونتو نصب کنید. انتخاب رمزگذاری، مسئولیت حفاظت و دسترسپذیری عبارت عبور را از بین نمیبرد؛ نسخهٔ بدون کلیدِ قابل بازیابی، برای روز حادثه فایدهای ندارد. مسیر خروجی را روی فضای کاربر و با مجوز محدود میسازیم.
# Ubuntu Bash: install only if GnuPG is not present.
command -v gpg
# If missing, install from the configured Ubuntu repositories:
# sudo apt update
# sudo apt install gnupg
export GPG_TTY="$(tty)"
umask 077
backup_dir=$(mktemp -d "$HOME/node-backup.XXXXXX")
release_to_backup=$(readlink -f /opt/darsavand-node/current)
printf 'Backup directory: %s\n' "$backup_dir"
printf 'Selected release: %s\n' "$release_to_backup"فقط اگر مقصد چاپشده یکی از انتشارهای r1 یا r2 همین آزمایشگاه بود ادامه دهید. در بلوک بعدی این محدودیت دوباره بهصورت ماشینی بررسی میشود. فایلهای کد یک انتشار کامل، واحد سرویس، فایل محیطی و دو فایل پیکربندی Nginx وارد آرشیو میشوند. اگر /etc/letsencrypt وجود داشته باشد، کل آن درخت هم وارد نسخهٔ رمزگذاریشده میشود تا پیوندهای live، فایلهای archive و اطلاعات تمدید در کنار هم بمانند. این انتخاب به معنی مجازبودن انتشار کلید خصوصی نیست؛ خروجی باید در فضای خصوصی و خارج از ماشین اصلی نگهداری شود. هنگام بستهگیری، انتشار تازه یا ویرایش پیکربندی انجام ندهید؛ همزمانی تغییرها میتواند اجزای ناسازگار در یک آرشیو بسازد. زمان تهیه و نسخهٔ انتخابشده را ثبت و نتیجهٔ بازسازی را بیازمایید. فایلهای log و تاریخچهٔ شل در این بستهٔ بازیابی برنامه وارد نمیشوند؛ نگهداری شواهد رخداد، سیاست جداگانهای دارد. آرشیوگیری از یک پایگاه دادهٔ زنده با کپی سادهٔ پوشه لزوماً سازگار نیست؛ در محصول دارای پایگاه داده باید ابزار و روش پشتیبان همان موتور را به کار برد. این نمونه فقط اجزای واقعی همین آزمایشگاه را پوشش میدهد.
(
set -euo pipefail
umask 077
test -d "$backup_dir"
test ! -e "$backup_dir/darsavand-node.tar.gz.gpg"
sudo -v
case "$release_to_backup" in
/opt/darsavand-node/releases/r1|/opt/darsavand-node/releases/r2) ;;
*) printf 'Unexpected release; stop\n' >&2; exit 1 ;;
esac
members=("${release_to_backup#/}"
etc/darsavand-node/app.env
etc/systemd/system/darsavand-node.service
etc/nginx/sites-available/darsavand-node
etc/nginx/conf.d/darsavand-trace.conf)
if sudo test -d /etc/letsencrypt; then members+=(etc/letsencrypt); fi
sudo tar -czf - -C / -- "${members[@]}" |
gpg --symmetric --cipher-algo AES256 \
--output "$backup_dir/darsavand-node.tar.gz.gpg"
cd "$backup_dir"
sha256sum darsavand-node.tar.gz.gpg > SHA256SUMS
)در نشست SSH تعاملی، GPG_TTY ترمینال فعلی را به ابزار دریافت عبارت عبور معرفی میکند؛ خطای Inappropriate ioctl for device معمولاً نشانهٔ نامناسببودن دسترسی ابزار پرسش به ترمینال است و با قراردادن رمز در خط فرمان حلش نمیکنیم. sudo -v پیش از زنجیره، اجازهٔ مدیریتی لازم برای خواندن فایلها را بررسی میکند تا پرسش sudo با پرسش رمزگذاری درهم نیامیزد. GnuPG باید عبارت عبور را تعاملی درخواست کند و خروجی رمزگذاریشده بسازد. اگر ابزار رمزگذاری شکست خورد، ممکن است فایل خروجیِ ناقص هم باقی بماند؛ وجود نام فایل موفقیت را ثابت نمیکند. آن را پشتیبان معتبر ننامید و برای تلاش مجدد یک پوشهٔ تازه بسازید. صحت مسیر رمزگشایی و بازیابی باید جداگانه آزموده شود. اگر ابزار رمزگذاری شکست خورد، موفقیت tar بهتنهایی کافی نیست؛ set -o pipefail شکست یک بخش زنجیره را آشکار میکند. در همان پوشه یک فایل چکیده داریم تا پس از انتقال، یکسانبودن بایتها را بررسی کنیم. SHA256 در این کاربرد خرابی انتقال را آشکار میکند؛ اگر مهاجم هم آرشیو و هم فایل چکیده را تغییر دهد، این دو فایل بهتنهایی اصالت را اثبات نمیکنند. فایل رمزگذاریشده و چکیده را از مسیر امن به محلی مستقل منتقل کنید و عبارت عبور را جدا نگه دارید. حفظ نسخه فقط روی همان دیسک سرور، در خرابی همان دیسک کمکی نمیکند. این آرشیو را به سایت درسآوند بارگذاری نکنید؛ فایل همراه آموزشی دوره فقط کد و دادهٔ ساختگی است و نیازی به دریافت پشتیبان واقعی سرور شما ندارد. پیش از حذف هر نسخهٔ قدیمی، وجود و قابلیت بازشدن نسخهٔ مورد نیاز را بر اساس سیاست نگهداری بررسی کنید.
قدم سوم: بازیابی را در پوشهٔ قرنطینه و ماشین تازه بیازمایید
# Recovery lab: replace this path with the folder received securely.
backup_dir="$HOME/node-backup-received"
restore_dir=$(mktemp -d "$HOME/node-restore.XXXXXX")
(
set -euo pipefail
umask 077
test -d "$restore_dir"
cd "$backup_dir"
sha256sum -c SHA256SUMS
gpg --decrypt darsavand-node.tar.gz.gpg |
tar --no-same-owner --no-same-permissions -xzf - -C "$restore_dir"
printf 'Quarantine directory: %s\n' "$restore_dir"
)پیش از این بلوک، فایل رمزگذاریشده و SHA256SUMS را با SFTP یا scp در یک پوشهٔ خصوصی روی ماشین آزمایش بازیابی قرار دهید و مسیر نمونهٔ node-backup-received را در کد عوض کنید. در این بلوک backup_dir باید مسیر واقعی پوشهٔ پشتیبان دریافتشده روی ماشین بازیابی باشد؛ متغیرهای ترمینال سرور قبلی خودکار منتقل نمیشوند. پیش از اجرا آن را با مسیر خودتان تنظیم کنید. پوشهٔ restore_dir تازه و خصوصی است و فایلهای بازیابیشده هنوز در /etc یا /opt ماشین اصلی قرار نگرفتهاند. آرشیو فقط باید از نسخهای باشد که خودتان ساخته و منبع آن را میشناسید؛ استخراج آرشیو ناشناس یا اجرای کد ناشناس مسئلهای جداگانه است. پیوندهای موجود در بخش گواهی را بهعنوان دادهٔ حساس بررسی کنید و از پوشهٔ قرنطینه، پیکربندی زنده را بیبررسی اجرا نکنید. درخت کد انتشار را پیدا کنید، فایلهای مورد انتظار را با شناسنامه تطبیق دهید و آزمونهای Node را با runtime مناسب اجرا کنید. این مرحله ساختار و کد را میآزماید؛ هنوز مالکیت root، حساب سرویس، مسیرهای نهایی و شبکهٔ مقصد بازسازی نشدهاند. فایلهای راز را برای راحتی مشاهده روی صفحه چاپ نکنید و مجوز پوشهٔ قرنطینه را عمومی نکنید.
قدم چهارم: اجزا را به ترتیب وابستگی بازسازی کنید
روی ماشین تازه ابتدا پیشنیازهای سیستم و همان خانوادهٔ پشتیبانیشدهٔ Node را مطابق درسهای دوم و سوم آماده کنید. سپس فقط حساب darsnode را از بخش ساخت حساب سرویس در درس پنجم آماده کنید. در این نقطه، پوشهٔ انتشار، پیوند current، فایل app.env و واحد این برنامه را نسازید؛ بلوک بازسازیِ پایین همین بخش، آنها را با شرط خالیبودن مقصد ایجاد میکند. پس از اجرای موفق آن بلوک و شروع سرویس، پیش از رفتن سراغ دامنه، درخواست مستقیم 127.0.0.1:3000/health را بررسی کنید. بعد Nginx و قالب لاگ را برگردانید، nginx -t را اجرا کنید و فقط در موفقیت، reload انجام دهید. اگر گواهیها همراه نسخهٔ رمزگذاریشده هستند، مسیر و مجوزهای ریشهای آنها باید مطابق نصب درست Certbot بازسازی شوند و نام دامنه و اعتبارشان دوباره بررسی شود. اگر گواهی ندارید، ابتدا پیکربندی HTTP اولیهٔ درس هفتم را به کار ببرید و مسیر صدور گواهی درس نهم را دوباره طی کنید؛ پیکربندی TLS با فایل گواهیِ مفقود را فعال نکنید. در پایان DNS و قوانین شبکه را با نشانی ماشین مقصد تطبیق دهید. موفقیت فقط در داخل ماشین برای تحویل خدمت عمومی کافی نیست.
برای بازگرداندن خود برنامه، بلوک زیر مسیر دقیق انتقال از قرنطینه به محل نهایی را نشان میدهد. آن را فقط روی ماشین تازهٔ بازیابی اجرا کنید؛ کاربر darsnode و runtime باید پیشتر مطابق درسهای سوم و پنجم ساخته شده باشند. مقدار restore_name را از شناسنامه انتخاب کنید: r1 یا r2. وجود هر مسیر مقصد باعث توقف میشود تا وضعیت موجود بیخبر بازنویسی نشود. این مرحله کد، فایل محیطی و واحد را برمیگرداند؛ هنوز پیکربندی TLS را فعال نمیکند. بعد از پیام فعالبودن سرویس، آزمون JSON سلامت درس دهم را اجرا کنید. سپس با دستورهای کامل درس هفتم، ابتدا Nginx را در HTTP بررسی و مسیر گواهی را طبق وضعیت نسخهٔ پشتیبان تکمیل کنید.
# Fresh recovery Ubuntu only; runtime and darsnode must already exist.
restore_name=r1
restore_source="$restore_dir/opt/darsavand-node/releases/$restore_name"
(
set -euo pipefail
case "$restore_name" in r1|r2) ;; *) exit 1 ;; esac
test -f "$restore_source/server.mjs"
test -f "$restore_dir/etc/darsavand-node/app.env"
test -f "$restore_dir/etc/systemd/system/darsavand-node.service"
sudo test ! -e /opt/darsavand-node/current
sudo test ! -L /opt/darsavand-node/current
sudo test ! -e "/opt/darsavand-node/releases/$restore_name"
sudo test ! -L "/opt/darsavand-node/releases/$restore_name"
sudo test ! -e /etc/darsavand-node/app.env
sudo test ! -L /etc/darsavand-node/app.env
sudo test ! -e /etc/systemd/system/darsavand-node.service
sudo test ! -L /etc/systemd/system/darsavand-node.service
sudo install -d -m 0755 /opt/darsavand-node/releases
sudo cp -R -- "$restore_source" "/opt/darsavand-node/releases/$restore_name"
sudo chown -R root:root "/opt/darsavand-node/releases/$restore_name"
sudo find "/opt/darsavand-node/releases/$restore_name" -type d -exec chmod 0755 {} +
sudo find "/opt/darsavand-node/releases/$restore_name" -type f -exec chmod 0644 {} +
sudo install -d -m 0700 /etc/darsavand-node
sudo install -o root -g root -m 0600 "$restore_dir/etc/darsavand-node/app.env" \
/etc/darsavand-node/app.env
sudo install -o root -g root -m 0644 "$restore_dir/etc/systemd/system/darsavand-node.service" \
/etc/systemd/system/darsavand-node.service
sudo ln -s "/opt/darsavand-node/releases/$restore_name" /opt/darsavand-node/current
sudo systemctl daemon-reload
sudo systemctl enable --now darsavand-node.service
sudo systemctl is-active darsavand-node.service
)آزمون نهایی باید چند لایه داشته باشد: سلامت JSON مستقیم، سلامت HTTPS عمومی، GET مجاز /api/tasks، ردشدن POST عمومی با ۴۰۳، پاسخ مورد انتظار برای مسیر ناموجود و ثبت لاگ درخواست. وضعیت سرویس، مقصد current، نسخهٔ Node و زمان بازیابی را نیز ثبت کنید. اگر یک آزمون شکست خورد، همان لایه را بررسی کنید و همهٔ تنظیمات را همزمان عوض نکنید. در سامانهٔ واقعی، آزمون مجوز کاربر و صحت دادهٔ تجاری هم لازم است. پشتیبان برنامه نمیتواند از خودش ثابت کند آخرین سفارشها یا تیکتها حفظ شدهاند؛ آن ادعا به شواهد مستقل داده نیاز دارد. هنگام تغییر DNS برای ماشین جایگزین، انتشار کش و تفاوت resolverها را در نظر بگیرید و ماشین قبلی را زودتر از اطمینان از مسیر بازیابی کنار نگذارید. بستهٔ این دوره روش نمونه را آموزش میدهد و تضمین زمان بازیابی برای هر ارائهدهنده یا زیرساخت نیست.
۱. پس از restart، فهرست کارها خالی است ولی health سالم است. پاسخ: وضعیت برنامه حافظهای است؛ برای نگهداری واقعی، پایگاه داده و سیاست ذخیره لازم است. ۲. برنامهٔ داخلی سالم و دامنه ۵۰۲ است. پاسخ: upstream، سوکت و error log Nginx را بررسی کنید. ۳. نام دامنه به IP قدیمی میرود. پاسخ: DNS و کش resolver را اصلاح و از بیرون بازآزمایی کنید؛ نصب Node علت را حل نمیکند. ۴. پس از بازیابی، nginx -t از فایل گواهی مفقود شکایت دارد. پاسخ: گواهی و کلید وابسته را مسئولانه بازیابی کنید یا از HTTP اولیه دوباره صدور معتبر انجام دهید. ۵. POST عمومی کد ۴۰۳ دارد. پاسخ: این رفتار حفاظتی مورد انتظار است؛ بازکردن نوشتن عمومی بدون احراز هویت، بخشی از این آزمایش نیست. ۶. سرویس با ۲۰۳/EXEC شکست میخورد. پاسخ: مسیر واقعی باینری، وجود آن و مجوز اجرا را بسنجید؛ تغییر DNS ربطی ندارد. ۷. r2 خراب است و فایلهای r1 وجود دارند. پاسخ: پیوند را با روش درس یازدهم برگردانید، restart و آزمون سلامت انجام دهید. ۸. آرشیو و چکیده پاساند ولی عبارت عبور گم شده است. پاسخ: یکسانبودن بایتها رمزگشایی را ممکن نمیکند؛ روش نگهداری کلید باید پیشاپیش آزموده شود. ۹. پشتیبان فقط روی دیسک خراب سرور بوده است. پاسخ: استقلال محل نگهداری فراهم نبوده است. ۱۰. همهٔ آزمونها سالماند اما کد API احراز هویت ندارد. پاسخ: این یک آزمایشگاه عمومیِ فقط خواندنی است و برای دادهٔ شخصی و عملیات واقعی هنوز طراحی کامل محصول لازم دارد.
تحویل نهایی و مسیر توسعهٔ بعدی
تحویل شما باید پنج بخش قابل بررسی داشته باشد: شناسنامهٔ محیط، نقشهٔ مسیر درخواست، نتیجهٔ آزمونهای مجاز و ممنوع، رسید انتشار و بازگشت، و گزارش یک بازیابی واقعی روی آزمایشگاه جدا. اسناد عمومی باید از راز و اطلاعات شخصی پاک باشند. برای ادامه، نخست مسئلهٔ واقعی محصول را بنویسید: چه کسی مجاز به دیدن یا تغییر کدام داده است؟ سپس ذخیرهٔ ماندگار، طراحی جدول یا مدل، مهاجرت نسخهها، احراز هویت، کنترل مجوز، محدودیت درخواست و نگهداری امن رازها را به نیاز همان محصول متصل کنید. هرکدام از این عنوانها ابزار و آزمون خودش را میخواهد؛ نصب پایگاه داده یا یک بستهٔ ورود، بهتنهایی محصول را آماده نمیکند. #validation-contracts و #try-catch را در مرز ورودی و شکست، و #async-await را در عملیات شبکه و پایگاه داده به کار خواهید برد. معیار پیشرفت این است که بتوانید از روی شواهد توضیح دهید برنامه چه میکند، در چه وضعیتی شکست میخورد و چگونه به حالت قابل استفاده برمیگردد.
برای مرور مهارتهای مرتبط، انتقال امن و آرشیو را از دورهٔ مقدماتی و طراحی راهبرد بازیابی را از دورهٔ پیشرفته بخوانید. در مسیر Node ویندوز نیز محدودیتهای پروژه و گامهای بعدی محصول مرور شدهاند.
انتقال امن فایل و پشتیبانگیری با SFTP، scp و tar: https://www.darsavand.com/courses/linux-server-from-zero?lesson=8706f5a1-9ec7-400e-886f-f74780325e25
راهبرد پشتیبان و بازیابی آزموده: https://www.darsavand.com/courses/linux-advanced-administration?lesson=596c0daa-c186-4081-95c6-bc0e1869f058
تحویل پروژه و مسیر بعدی؛ از ویندوز محلی تا بکاند واقعی: https://www.darsavand.com/courses/nodejs-windows-backend-start?lesson=951ecfa0-3b0a-4c38-98f1-82d429cd9c08
منابع رسمی انگلیسی:
GNU tar manual: https://www.gnu.org/software/tar/manual/tar.html
GnuPG manual — symmetric encryption: https://www.gnupg.org/documentation/manuals/gnupg/Operational-GPG-Commands.html
Certbot certificate files and renewal: https://eff-certbot.readthedocs.io/en/stable/using.html
Ubuntu Server shell backup: https://ubuntu.com/server/docs/how-to/backups/back-up-using-shell-scripts/
Node.js filesystem: https://nodejs.org/docs/latest-v24.x/api/fs.html
تألیف و سناریوهای اختصاصی: تحریریه درسآوند. هیچ رمز، کلید خصوصی یا پشتیبان واقعی کاربر برای تکمیل این دوره دریافت یا منتشر نمیشود.
متن درسهای عمومی را بدون ورود بخوانید.