كل ما بنيته عبر هذا الدرس — الوضع الصارم، والمتغيرات، والشرطيات، والحلقات، والدوال، وإعادة التوجيه، ومعالجة النصوص، ومعالجة الأخطاء، وتحليل الوسائط — يتلاقى هنا. يبني هذا المشروع الختامي سكريبت نسخ احتياطي على مستوى الإنتاج من الصفر، من النوع الذي ستجده يعمل ليلًا على خوادم قواعد البيانات في المؤسسات الهندسية المتوسطة والكبيرة. سكريبتات النسخ الاحتياطي خادعة في سهولة الوقوع في أخطائها: تبدو وكأنها تعمل حتى اللحظة التي تحتاج فيها فعليًا إلى استعادة، فتكتشف أن الأرشيف كان تالفًا، أو أن منطق الاحتفاظ حذف كل شيء، أو أن رسائل الفشل ابتُلعت بصمت.
سنبني backup.sh بشكل طبقي، شارحين الأساس المنطقي الإنتاجي في كل خطوة.
الطبقة الأولى: حزام الأمان وكتلة الإعداد
كل سكريبت نسخ احتياطي إنتاجي يبدأ بنفس السطرين وكتلة إعداد للقيم الثابتة. يجب أن تكون كتلة الإعداد هي المصدر الوحيد للحقيقة — لا سلاسل سحرية مدفونة في المنطق.
فكرة رئيسية — الإعداد في الأعلى، والأسرار من الملفات: كل قيمة قابلة للضبط تقطن في كتلة readonly. كلمة مرور قاعدة البيانات لا تُكتَب أبدًا بشكل مباشر — بل تُقرأ من ملف مملوك لـ root بصلاحيات 600 (DB_PASS_FILE). هذا يلبي متطلبات أدوات فحص الأسرار مثل GitGuardian وtruffleHog، ويتطابق مع النمط المستخدم في PostgreSQL's .pgpass وMySQL's --defaults-extra-file.
الطبقة الثانية: التسجيل والإشعارات والتنظيف
النسخ الاحتياطي الذي يفشل بصمت أسوأ من عدم وجود نسخة احتياطية. يجب أن يحمل كل سطر سجل طابعًا زمنيًا ومستوى خطورة واسم السكريبت حتى يستطيع grep إيجاده فورًا في مجمّع سجلات مشغول كـ Datadog أو Splunk. تضمن trap على EXIT إزالة الأرشيفات الجزئية حتى لو أُوقف السكريبت في منتصف الكتابة.
الفشل بسرعة وبوضوح قبل لمس أي بيانات. تمنع الفحوصات الأولية الفشل الجزئي الصامت الذي ينتج عنه أرشيف فارغ مع كود خروج 0.
require_command() {
command -v "$1" >/dev/null 2>&1 || die "الأمر المطلوب غير موجود: $1"
}
check_free_disk() {
local dir="$1"
local required_gb="$2"
local free_gb
free_gb=$(df -BG "$dir" | awk 'NR==2 {gsub(/G/,""); print $4}')
(( free_gb >= required_gb )) \
|| die "مساحة قرص غير كافية على $dir: ${free_gb}G متاح، مطلوب ${required_gb}G"
log_info "القرص بخير: ${free_gb}G متاح على $dir"
}
preflight() {
log_info "--- الفحوصات الأولية ---"
require_command mysqldump
require_command gzip
require_command tar
require_command openssl
[[ -f "$DB_PASS_FILE" ]] \
|| die "ملف كلمة مرور قاعدة البيانات غير موجود: $DB_PASS_FILE"
[[ "$(stat -c %a "$DB_PASS_FILE")" == "600" ]] \
|| die "ملف كلمة المرور له صلاحيات غير آمنة (يجب أن تكون 600): $DB_PASS_FILE"
mkdir -p "${BACKUP_ROOT}/db" "${BACKUP_ROOT}/files" "${BACKUP_ROOT}/logs"
check_free_disk "$BACKUP_ROOT" "$MIN_FREE_GB"
log_info "اجتازت الفحوصات الأولية"
}
فخ إنتاجي — صلاحيات غير محققة على ملفات بيانات الاعتماد: كثير من الفرق تنشئ ملف كلمة المرور لكن تنسى تأمينه. إذا كان backup_user.pass قابلًا للقراءة من الجميع، فأي عملية على المضيف تستطيع قراءة كلمة مرور قاعدة البيانات. يُطبّق فحص stat -c %a وضع 600 في وقت التشغيل، فيفشل نشر مُهيَّأ بشكل خاطئ بصوت عالٍ بدلًا من تسريب بيانات الاعتماد بصمت.
الطبقة الرابعة: دوال النسخ الاحتياطي
افصل تفريغ قاعدة البيانات عن أرشيف الملفات. يتيح هذا إعادة محاولة كل خطوة أو اختبارها أو مراقبتها بشكل مستقل. الخيار --single-transaction لـ MySQL حرج في الإنتاج: يأخذ لقطة متسقة دون الحصول على أقفال الجداول، لذا يستمر تطبيقك في خدمة الحركة أثناء نافذة النسخ الاحتياطي.
backup_database() {
log_info "--- نسخ احتياطي لقاعدة البيانات ---"
local db_archive="${BACKUP_ROOT}/db/db_${TIMESTAMP}.sql.gz"
CURRENT_ARCHIVE="$db_archive"
local db_pass
db_pass="$(cat "$DB_PASS_FILE")"
if $DRY_RUN; then
log_info "[DRY-RUN] سيتم تفريغ $DB_NAME إلى $db_archive"
return 0
fi
# --single-transaction: لقطة متسقة بدون أقفال جداول (InnoDB)
# MYSQL_PWD يمنع ظهور كلمة المرور في مخرجات `ps aux`
MYSQL_PWD="$db_pass" mysqldump \
--user="$DB_USER" \
--single-transaction \
--routines \
--events \
--quick \
"$DB_NAME" \
| gzip -9 > "$db_archive"
gzip -t "$db_archive" || die "فشل فحص سلامة أرشيف قاعدة البيانات: $db_archive"
local size_mb
size_mb=$(du -m "$db_archive" | awk '{print $1}')
log_info "أرشيف قاعدة البيانات سليم: $db_archive (${size_mb} ميجابايت)"
CURRENT_ARCHIVE=""
}
backup_files() {
log_info "--- نسخ احتياطي للملفات ---"
local files_archive="${BACKUP_ROOT}/files/files_${TIMESTAMP}.tar.gz"
CURRENT_ARCHIVE="$files_archive"
if $DRY_RUN; then
log_info "[DRY-RUN] سيتم أرشفة $FILES_DIR إلى $files_archive"
return 0
fi
tar -czf "$files_archive" \
--exclude="$FILES_DIR/framework/cache" \
--exclude="$FILES_DIR/framework/sessions" \
--exclude="$FILES_DIR/logs" \
-C "$(dirname "$FILES_DIR")" \
"$(basename "$FILES_DIR")"
tar -tzf "$files_archive" >/dev/null || die "فشل فحص سلامة أرشيف الملفات: $files_archive"
local size_mb
size_mb=$(du -m "$files_archive" | awk '{print $1}')
log_info "أرشيف الملفات سليم: $files_archive (${size_mb} ميجابايت)"
CURRENT_ARCHIVE=""
}
الطبقة الخامسة: تطبيق سياسة الاحتفاظ
منطق الاحتفاظ هو حيث تُدمّر سكريبتات النسخ الاحتياطي نفسها في أغلب الأحيان. تعبير find -mtime +N -delete هو المعيار الصناعي؛ تجنب صياغة حلقات يدوية تقارن التواريخ كسلاسل نصية — فهي تنكسر عند حدود الأشهر والسنوات.
enforce_retention() {
log_info "--- تطبيق سياسة احتفاظ ${RETENTION_DAYS} يومًا ---"
local deleted_count=0
while IFS= read -r -d '' old_file; do
if $DRY_RUN; then
log_info "[DRY-RUN] سيتم حذف: $old_file"
else
rm -f "$old_file"
log_info "حُذف أرشيف قديم: $old_file"
fi
(( deleted_count++ )) || true
done < <(find "$BACKUP_ROOT" \
\( -name "db_*.sql.gz" -o -name "files_*.tar.gz" \) \
-mtime "+${RETENTION_DAYS}" \
-print0)
log_info "انتهى مسح الاحتفاظ: حُذف ${deleted_count} أرشيف(ات) قديمة"
}
ممارسة احترافية — استخدم -print0 مع read -d '': يمكن لأسماء الملفات أن تحتوي قانونيًا على مسافات وأسطر جديدة. يستخدم زوج find -print0 / read -d '' بايت الصفر كمفصل، والذي لا يمكن أن يظهر في اسم ملف. هذا هو النمط الإلزامي في دليل أسلوب Shell من Google للتكرار الآمن على مخرجات find.
الطبقة السادسة: تحليل الوسائط و main()
يقبل السكريبت علمَي --dry-run و--verbose. يتيح وضع الجفاف للمشغلين التحقق من خطة النسخ الاحتياطي في بيئة جديدة دون لمس أي بيانات أو هدر مساحة القرص.
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) DRY_RUN=true ; shift ;;
--verbose) VERBOSE=true ; shift ;;
--help|-h)
echo "الاستخدام: $SCRIPT_NAME [--dry-run] [--verbose]"
exit 0
;;
*) die "وسيطة غير معروفة: $1" ;;
esac
done
}
main() {
parse_args "$@"
mkdir -p "$(dirname "$LOG_FILE")"
log_info "========================================="
log_info "بدأ النسخ الاحتياطي (المضيف: $(hostname)، المستخدم: $(whoami))"
$DRY_RUN && log_warn "وضع DRY-RUN: لن تُكتب أي ملفات"
preflight
backup_database
backup_files
enforce_retention
local db_size files_size
db_size=$(du -sh "${BACKUP_ROOT}/db/db_${TIMESTAMP}.sql.gz" 2>/dev/null | awk '{print $1}' || echo "N/A")
files_size=$(du -sh "${BACKUP_ROOT}/files/files_${TIMESTAMP}.tar.gz" 2>/dev/null | awk '{print $1}' || echo "N/A")
log_info "اكتمل النسخ الاحتياطي. قاعدة البيانات: $db_size الملفات: $files_size"
log_info "========================================="
notify_slack ":white_check_mark: *نجح النسخ الاحتياطي* على $(hostname). قاعدة البيانات: ${db_size}، الملفات: ${files_size}"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
مسار تنفيذ سكريبت النسخ الاحتياطي الإنتاجي. تُطلَق شبكة الأمان trap على أي مسار خروج، مما يضمن عدم تراكم الأرشيفات الجزئية.
التشغيل والجدولة
اجعل السكريبت قابلًا للتنفيذ واختبره في وضع dry-run قبل الجدولة:
# الإعداد لمرة واحدة
chmod +x /opt/scripts/backup.sh
chown root:root /opt/scripts/backup.sh
chmod 700 /opt/scripts/backup.sh
# التحقق بدون كتابة بيانات
/opt/scripts/backup.sh --dry-run --verbose
# تشغيل حقيقي (اختبر قبل إضافته إلى cron)
/opt/scripts/backup.sh --verbose
# أضف إلى crontab الخاصة بـ root — يعمل عند 02:00 كل يوم
crontab -e
# أضف هذا السطر:
# 0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
# مراقبة التشغيلات الأخيرة
grep "اكتمل النسخ الاحتياطي\|فشل النسخ الاحتياطي" /var/log/backup.log | tail -20
ممارسة احترافية — تحقق من الاستعادة بجدول منتظم: نسخة احتياطية لم تُستعَد قط هي فرضية لا ضمان. في شركات مثل Stripe، تُشغَّل اختبارات استعادة آلية أسبوعيًا في بيئة التجهيز: تشغيل قاعدة بيانات مؤقتة، وتحميل أحدث نسخة تفريغ، وتشغيل استعلام تحقق، ثم التخلص منها. هذا هو الدليل الوحيد الموثوق على أن نسختك الاحتياطية قابلة للاستخدام.
يُجسّد هذا السكريبت كل درس في هذا الكورس: set -euo pipefail يكتشف الأخطاء الصامتة، وtrap يتعامل مع الخروج غير المتوقع، والدوال تعزل الاهتمامات، وreadonly يمنع التعديل العرضي، و--dry-run يُتيح التحقق الآمن، والتسجيل المنظم يجعل كل تشغيل قابلًا للتدقيق. هذا هو البرمجة بالشل على مستوى الإنتاج.
نستخدم ملفات تعريف الارتباط لتشغيل هذا الموقع وتحليل الزيارات وعرض إعلانات مخصّصة. يمكنك قبول كل ملفات تعريف الارتباط أو رفض غير الأساسية منها.
سياسة الخصوصية