النسخ الاحتياطي والاستعادة
نسخ يومي لكل بيئة
يأخذ صباربيز نسخة احتياطية من كل بيئة مرة يوميًا وهي تعمل، ويحتفظ بآخر سبع نسخ لكل بيئة. لا يحتاج أي إعداد. كل نسخة تحمل قاعدة بيانات البيئة (المستخدمين، وبصمات كلمات المرور، والجداول، وبيانات Storage الوصفية) وملفات Storage الخاصة بها، مع بصمة لكل منها. إذا فشلت نسخة يصل إشعار عبر قنوات المشغّل.
| الإعداد | الافتراضي | المعنى |
|---|---|---|
SBARBASE_BACKUP_HOUR |
3 |
ساعة التشغيل اليومي بتوقيت UTC؛ القيمة off توقفه |
SBARBASE_BACKUP_KEEP |
7 |
عدد النسخ المحفوظة لكل بيئة. لا تُحسب النسخ المأخوذة لترقية: تُحفظ نسخ آخر 3 ترقيات نقلت نسخة المستودع (الترقيات) |
اضبطهما في متغيرات خدمة systemd، أو مرّرهما إلى deploy/compose.sh في كل up مع Docker (انظر الإعدادات).
الأوامر (مع Docker أضف قبلها docker compose exec sbarbase):
| المهمة | الأمر |
|---|---|
| نسخ بيئة واحدة الآن | python3 lab/backup.py create <environment> |
| نسخ كل البيئات الآن | python3 lab/backup.py create all |
| عرض النسخ | python3 lab/backup.py list |
| استعادة بيئة | python3 lab/backup.py restore <environment> <backup> |
| عرض أوامر التعافي المطلوبة | python3 lab/backup.py restore-status |
| معالجة انقطاع استبدال البيانات | python3 lab/backup.py recover-restore <scope> <backup> <restore-stamp> |
| إكمال تشغيل الخدمات بعد استعادة البيانات | python3 lab/backup.py complete-restore <scope> <backup> <restore-stamp> |
| حذف ما احتفظت به الاستعادة جانبًا | python3 lab/backup.py discard-previous <environment> |
| استعادة بيانات Storage المشتركة | python3 lab/backup.py restore-storage <backup> |
| حذف ما احتفظت به تلك الاستعادة جانبًا | python3 lab/backup.py discard-previous storage |
| استعادة قاعدة الإدارة | python3 lab/backup.py restore-management <backup> |
| حذف ما احتفظت به تلك الاستعادة جانبًا | python3 lab/backup.py discard-previous management |
| عرض المجموعات على الهدف خارج الخادم | python3 lab/backup.py offsite-list |
| جلب مجموعة واحدة من الهدف | python3 lab/backup.py offsite-fetch <backup> |
| الاستعادة من الهدف | python3 lab/backup.py restore <environment> <backup> --offsite |
<environment> هو معرّف البيئة من الواجهة، أو معرّف التشغيل (e_...، وهو المسار في رابط الـ API الخاص بها). أدخل معرّف التشغيل أو storage أو management مكان <scope>. <backup> هو الوقت الذي يعرضه list، مثل 20260924T030000Z.
الاستعادة تستبدل بيانات البيئة بالصفوف والمستخدمين والملفات المحفوظة في النسخة. وترفض قبل أن تغيّر شيئًا إذا كانت حاوية Auth أو REST أو Realtime الخاصة بالبيئة على صورة غير التي يثبّتها هذا الإصدار، ولا تشغّل أيًا منها على بيانات مستعادة. تتوقف خدمات Auth وREST وRealtime المهيأة لتلك البيئة. ويتوقف Storage المشترك أيضا، فتتعطل قراءة الملفات وكتابتها في جميع البيئات حتى يعود إلى العمل. يغلق البرنامج قواعد البيانات أمام الاتصالات الجديدة وينهي اتصالات الجهات المسموح لها بالكتابة قبل الاستبدال. يحتفظ بالقاعدة القديمة وملفاتها حتى تحذفها بالأمر discard-previous.
إذا توقف البرنامج بعد بدء استبدال البيانات، يبقى سجل العملية محفوظا. اتبع أمر recover-restore إذا ظهرت رسالة خطأ تتضمنه. وقد يتوقف البرنامج بالقوة دون رسالة؛ بعد توقفه، شغّل python3 lab/backup.py restore-status ليعرض الأمر المطلوب بمعرّف البيئة أو storage أو management ووقت النسخة ووقت الاستعادة. يقرأ أمر عرض الحالة السجلات دون تغييرها أو الاتصال بـ Docker، فلا تحتاج إلى تخمين هذه القيم.
يتحقق أمر التعافي من هوية قواعد البيانات والنسخة. إذا كانت القاعدة المستعادة قد فُتحت للكتابة، يحافظ عليها وعلى البيانات والملفات الجديدة. أما قبل إعادة الحالة السابقة، فيتحقق أيضا من هوية الملفات. لا تكرر restore ولا تحذف السجل لتجاوز هذا القرار. ما زال اختبار الانقطاع الفعلي جاريا؛ نجاح استعادة عادية أو اختبار أحد الأجزاء لا يثبت سلامة التعافي كله.
يدعم هذا المسار PostgreSQL 17 والعمليات الخلفية المصنفة في فحوصه. ويدعم أيضا الإعدادات الافتراضية للصورة المثبتة من Supabase، بشرط أن يستخدم Cron اتصالات عادية، وأن تعمل عمليات Cron وpg_net في قاعدة postgres المنفصلة عن القاعدة التي تستعاد. ويتحقق من إعداد supautils وsafeupdate الأصلي للدور authenticator. ويرفض أي تغيير في هذه الإعدادات أو إصدارات الإضافات، وأي اشتراكات أو عمليات خلفية غير معروفة. يجب أن تكون أدوار المالك وأصحاب الصلاحيات الواردة في النسخة موجودة مسبقا. يأخذ البرنامج ملكية القاعدة وصلاحياتها وتعليقها وحد الاتصالات وإعداداتها وإعدادات الأدوار الخاصة بها من النسخة نفسها.
إذا استعيدت البيانات ثم تعذر تشغيل الخدمات أو نشر عناوينها أو التحقق من جاهزيتها، ينتهي الأمر برمز خطأ ويحفظ حالة الاستعادة في ملف خاص داخل .lab/upstream/restore-completions/. لا يعيد قاعدة البيانات السابقة ولا يستعيد النسخة مرة ثانية. اتبع أمر complete-restore الذي تظهره رسالة الخطأ، وأدخل وقت الاستعادة كما هو، بحرفي t وz الصغيرين. مثال:
python3 lab/backup.py complete-restore e_<24-hex-digits> 20260924T030000Z 20261003t070000z
python3 lab/backup.py complete-restore storage 20260924T030000Z 20261003t070100zيتحقق أمر الإكمال من بصمات النسخة وملكية الحاوية وصورتها ومعرّف القاعدة OID والحالة المحفوظة. ويعيد محاولة تشغيل الخدمات ونشر عناوينها وفحص جاهزيتها، ومنها تشغيل Storage المشترك بعد استعادة البيئة. يحافظ على الكتابات التي أُضيفت بعد فتح القاعدة، ولا يقارنها بالنسخة القديمة. وعند نجاح الخطوات يحفظ سجل الإكمال. يتحقق تكرار الأمر نفسه من الهوية دون إعادة تشغيل الخدمات. ينتظر فحص البيئة استجابة HTTP 200 من Auth وفتح منفذ TCP في Storage؛ ولا يثبت جاهزية واجهات REST أو Realtime أو Storage. كذلك لا تثبت محاكاة الخدمات في الاختبارات جاهزية خدمات Supabase الفعلية.
ما دامت هناك استعادة تنتظر الجاهزية، أو حالة محفوظة يتعذر التحقق منها، يرفض البرنامج استعادة أخرى أو حذف الحالة السابقة أو حذف النسخ القديمة، بما فيها نسخ التثبيت. ويرفض إنشاء نسخة جديدة قبل كتابة ملفاتها. تبقى النسخة التي يشير إليها سجل الإكمال الحالي محفوظة حتى يمكن التحقق منه مجددا. وعند حذف البيانات السابقة أو النسخ القديمة، يتحقق البرنامج من سجل الإكمال المحفوظ دون اشتراط بقاء الحاوية القديمة مثبتة. أما أمر الإكمال نفسه فيتحقق من هوية قاعدة البيانات الحالية. تستبدل الاستعادة اللاحقة سجل تلك البيئة، فتزول هذه الحماية الإضافية عن النسخة القديمة. لا تحذف ملفات الحالة ولا تعدّلها لتجاوز الرفض، ولا تكرر restore لحل مشكلة جاهزية الخدمات. إذا تغيّرت هوية القاعدة المستعادة أو النسخة، افحص الحالة الحالية وعالج الاختلاف قبل أي إجراء تعاف آخر.
تختبر الفحوص الحالية إنشاء النسخ ومطابقة بيانات الاستعادة. ويبقى اختبار انقطاع العملية والتعافي الفعلي منها مطلوبا قبل اعتماد هذه الطريقة.
بيان التثبيت. يكتب كل تشغيل يومي أيضًا المجلد .lab/backups/installation/<backup>/، وفيه: صور الحاويات المثبّتة الإصدار، وتوجيه كل بيئة، وعملاء الفهرس ومشاريعه وبيئاته وعضوياته ومهامه، وإعدادات المشغّل، وأسماء الملفات في .secrets/. لا يحوي أي قيمة سرية: يُبنى من قوائم حقول مسموحة، ويُختصر عنوان الـ webhook إلى اسم مضيفه. يحمل بصمته، ويُحفظ ما دامت نسخ البيئات محفوظة. على خادم جديد يخبرك بالإصدارات والأسرار التي تحتاجها النسخ.
بيانات Storage المشتركة. كل تشغيل للأمر create all، اليومي منه والذي يسبق كل ترقية، يأخذ أيضًا نسخة من storage_metadata، وهي قاعدة البيانات الوحيدة التي يحفظها Storage لكل البيئات: تسجيل Storage لكل بيئة، ومفاتيح توقيعها، وحالة الترحيل التي يسجّلها Storage لها. تُحفظ في .lab/backups/storage/<backup>/: تفريغ يُؤخذ داخل لقطة واحدة وStorage يعمل، مع بيان خاص بها فيه بصمتها ومعرّفات البيئات التي تسجّلها، ولا شيء سري. وإذا فشل التفريغ فشل التشغيل كله. يستبدل الأمر restore-storage <backup> القاعدة كلها بها. يتوقف Storage لكل البيئات في أثناء ذلك؛ وتُحفظ القاعدة الحالية جانبًا حتى discard-previous storage، وإذا انقطعت العملية، اتبع تعليمات التعافي أعلاه واستخدم storage مكان معرّف البيئة. ويرفض نسخة لا تسجّل بيئة منشورة الآن، لأن Storage تلك البيئة سيفقد تسجيله. استعدها مع نسخ البيئات من التشغيل نفسه، والبيئات أولًا: حالة الترحيل التي تسجّلها يجب أن تطابق قواعد بياناتها (الترقيات). وتنتقل ضمن مجموعة التشغيل المشفّرة أدناه؛ أما نسخ lab/offsite.py الخاصة بكل بيئة فلا تشملها.
قاعدة الإدارة. كل تشغيل للأمر create all يأخذ أيضًا نسخة من management، وهي قاعدة Auth الخاص بنطاق الإدارة: حسابات المشغّلين، وعوامل الدخول الخاصة بهم، وجلساتهم. إصدار Auth يرحّلها كما يرحّل قواعد Auth في البيئات، لذلك هذه النسخة هي طريق العودة لها. تُحفظ في .lab/backups/management/<backup>/: تفريغ يُؤخذ داخل لقطة واحدة وAuth الإدارة يعمل، مع بيان خاص بها فيه بصمتها وعدد حسابات المشغّلين وهوياتهم، ولا شيء سري. وإذا فشل التفريغ فشل التشغيل كله. يستبدل الأمر restore-management <backup> القاعدة كلها بها عبر التحويل المحاط بالسياج نفسه الذي تستخدمه الاستعادات الأخرى، ويتحقق من عدد حسابات المشغّلين مقارنة بالنسخة قبل أي إعادة تسمية. يتوقف Auth الإدارة وعملية Storage المشتركة في أثناء ذلك، فلا يستطيع المشغّلون تسجيل الدخول ويتوقف Storage لكل البيئات، ولا يتوقف شيء غير ذلك. وتُحفظ القاعدة الحالية جانبًا حتى discard-previous management، وإذا انقطعت العملية، اتبع تعليمات التعافي أعلاه واستخدم management مكان معرّف البيئة. حسابات المشغّلين وكلمات مرورها وعوامل دخولها وجلساتها التي تغيّرت بعد النسخة تعود إلى ما كانت عليه وقتها. لا تقرأ لوحة الإدارة عنوان Auth الإدارة إلا عند بدء تشغيلها: إذا أعادت الاستعادة Auth الإدارة على عنوان جديد، تنبّه إلى ذلك الأوامر restore-management وcomplete-restore وrecover-restore وrestore-status إلى أن يُعاد تشغيل صباربيز، ويجب عندها إعادة تشغيل صباربيز قبل أن يستطيع المشغّلون تسجيل الدخول من جديد. وإذا أُعيد التشغيل خلال نحو عشر ثوانٍ من تغيّر العنوان، فلا يمكن تمييزه عن لوحة بدأت قبل التغيّر، فقد يبقى التنبيه بعده؛ أعد التشغيل مرة أخرى فيختفي. وتنتقل ضمن مجموعة التشغيل المشفّرة أدناه إذا أخذ التشغيل نفسه نسخة من بيئة واحدة على الأقل؛ أما التشغيل الذي لا يشمل أي بيئة فلا يُنسخ خارج الخادم، لذلك تبقى حسابات المشغّلين على هذا الخادم وحده في تثبيت لا بيئة فيه بعد. ولا تشملها نسخ lab/offsite.py الخاصة بكل بيئة.
هناك طريقتان مستقلتان لنسخ النسخ الاحتياطية خارج الخادم، موصوفتان أدناه. اختر واحدة منهما. ننصح بمجموعات التشغيل المشفّرة (الطريقة الثانية): كل مجموعة تحمل نسخ البيئات، وبيانات Storage المشتركة، وقاعدة الإدارة، وبيان التثبيت، وهذا ما يحتاجه خادم جديد بعد فقد القديم. وقد نجحت في 2026-10-17 على الخادم الحقيقي استعادة من مجموعة تشغيل في Cloudflare R2 وحدها.
نسخ خارج الخادم
تُكتب النسخ في .lab/backups/ على هذا الخادم. لتنجو من فقدان الخادم، دع صباربيز ينسخ كل نسخة جديدة، مشفّرة، إلى تخزين متوافق مع S3: Cloudflare R2 (10 GB مجانًا)، أو Backblaze B2، أو AWS S3، أو Wasabi، أو MinIO خاص بك.
- أنشئ bucket ومفتاح وصول يستطيع القراءة والكتابة والعرض والحذف فيه.
- أعطِ صباربيز الإعدادات مرة واحدة. تُقرأ من stdin، لا من سطر الأوامر أبدًا، وتُحفظ في
.secrets/offsite.json:
python3 lab/offsite.py configure <<'JSON'
{"endpoint": "https://<account>.r2.cloudflarestorage.com", "bucket": "my-backups", "region": "auto",
"access_key_id": "…", "secret_access_key": "…", "passphrase": "a long phrase only you know", "keep": 30}
JSONيتحقق من الإعدادات بكتابة كائن تجريبي وقراءته وحذفه. بعدها ينسخ النسخ الاحتياطي اليومي كل نسخة جديدة وحده، ويحتفظ بأحدث 30 لكل بيئة في الـbucket؛ والنسخ الفاشل يُبلَّغ عنه كما يُبلَّغ عن النسخ الاحتياطي الفاشل.
يُشفَّر كل ملف على هذا الخادم قبل أن يغادره (AES-256-GCM، والمفتاح مشتق من عبارة المرور)، فلا يرى مزوّد التخزين بياناتك أبدًا. احفظ عبارة المرور في مكان غير هذا الخادم: من دونها لا يمكن قراءة النسخ، لا لك ولا لأحد.
| المهمة | الأمر |
|---|---|
| نسخ ما لم يُنسخ بعد | python3 lab/offsite.py push |
| عرض النسخ في الـbucket | python3 lab/offsite.py list |
| إعادة نسخة إلى هذا الخادم | python3 lab/offsite.py fetch <environment> <backup> |
تُطابَق النسخة المُعادة مع ملف البيان الخاص بها، ثم تُستعاد بـ backup.py restore كالمعتاد. أما النسخة الاحتياطية التي تأخذها الترقية لنفسها فتبقى محلية (backup.py create all --local-only)، فلا يمنع تخزينٌ لا يمكن الوصول إليه أي ترقية.
يشغّل CI هذه الدورة على جهاز نظيف مع كل تغيير أمام MinIO مؤقت: نسخة تُنسخ وحدها، ولا شيء في الـbucket إلا نص مشفّر، ورفض عبارة مرور خاطئة، ثم إعادة النسخة واستعادتها (الدليل).
مجموعات التشغيل المشفّرة خارج الخادم
تُكتب النسخ في .lab/backups/ على هذا الخادم. لتحتفظ بنسخة في مكان آخر، وجّه صباربيز إلى مخزن (bucket) واحد متوافق مع S3، مثل Amazon S3 وCloudflare R2 وBackblaze B2 وMinIO. بعد كل تشغيل يومي تُجمع نسخ التشغيل وبيان التثبيت في ملف واحد، ويُشفَّر بخوارزمية AES-256-GCM، ويُرفع باسم <prefix><backup>.sbb. يحتفظ الهدف بعدد مجموعات يساوي SBARBASE_BACKUP_KEEP؛ تُحذف المجموعات الأقدم تحت البادئة نفسها، ولا يُمس أي كائن آخر.
أنشئ مفتاح التشفير. لا ينشئه صباربيز من تلقاء نفسه:
python3 lab/backup.py offsite-key .secrets/upstream/offsite-key.jsonصلاحية الملف 0600. احفظ نسخة منه بعيدًا عن هذا الخادم: بدونه لا يمكن فك تشفير أي نسخة، ومن يملكه مع المخزن يقرأ كل النسخ.
ضع مفتاح الوصول إلى المخزن في ملف بصلاحية 0600، مثل
.secrets/upstream/offsite-s3.json:{"schema": 1, "accessKeyId": "<access key id>", "secretAccessKey": "<secret access key>"}امنح هذا المفتاح صلاحيات العرض والقراءة والكتابة والحذف على هذا المخزن فقط.
اكتب الملف
.lab/upstream/backup-offsite.json:{ "schema": 1, "s3": {"endpoint": "https://s3.eu-central-1.amazonaws.com", "region": "eu-central-1", "bucket": "example-backups", "prefix": "sbarbase/", "credentialsFile": ".secrets/upstream/offsite-s3.json"}, "keyFile": ".secrets/upstream/offsite-key.json" }يجب أن يستخدم العنوان
https. المسارات نسبية إلى نسخة المستودع.
من التشغيل اليومي التالي تُرفع كل نسخة يومية تلقائيًا. إذا فشل الرفع (تعذّر الوصول إلى المخزن، أو كان ملف المفتاح مفقودًا أو مقروءًا لغيرك) يصل إشعار backup.failed ويُطبع السبب؛ لا تتغير النسخ المحلية ولا تُحذف، ولا يُحذف شيء على الهدف. قد يصلك عندها إشعاران: نسخة اكتملت، ونسخ خارجي فشل.
يُنسخ التشغيل اليومي (create all) وحده؛ النسخة اليدوية لبيئة واحدة تبقى على هذا الخادم.
استعد من الهدف على هذا الخادم، أو على خادم جديد نُشرت عليه البيئة نفسها. يحتاج الخادم الجديد أولًا إلى الملفات الثلاثة أعلاه: الإعداد، وملف بيانات الاعتماد، ونسختك من المفتاح.
python3 lab/backup.py offsite-list
python3 lab/backup.py restore <environment> <backup> --offsiteتُنزَّل المجموعة، ويُفحص وسم المصادقة قبل فك أي شيء، ثم تُطابق كل نسخة مع بصماتها قبل أن يستخدمها restore. النسخة الموجودة أصلًا على الخادم تبقى كما هي.
الاستعادة على تثبيت جديد
تُستعاد النسخة إلى بيئة موجودة ومنشورة. أما على خادم جديد بعد فقدان القديم، فالبيئة لم تُنشأ بعد. لذلك تسجّل كل نسخة معرّفات عميلها ومشروعها وبيئتها (الحقل ownership في manifest.json، منذ 2026-09-23)، فيعيد التثبيت الجديد إنشاءها كما كانت ثم يستعيد النسخة إليها:
- ثبّت صباربيز على الخادم الجديد (إعداد المشغّل) بالإصدار نفسه، لتتطابق صور الحاويات المثبّتة؛ يذكرها بيان التثبيت.
- ضع النسخة في
.lab/backups/<runtime>/<backup>/: انسخ المجلد من حيث تحفظه، أو نزّله من الهدف الخارجي كما في القسم السابق. - نفّذ الخطوات الثلاث التالية بحساب مشغّل التثبيت. يعيد
relinkإنشاء العميل والمشروع والبيئة بمعرّفاتها الأصلية ومعرّف التشغيل نفسه، ثم يجهّز العامل بيئة فارغة بهذا الاسم. وحين يعرضهاenvironmentsبالحالةsucceeded، يملؤهاrestoreمن النسخة.
sbarbase relink <runtime> <backup>
sbarbase environments
sbarbase restore <runtime> <backup>الأمر relink حذر عن قصد. لا يربط النسخة بعميل أو مشروع بالاسم أبدًا: إذا وُجد هنا عميل بالاسم المسجل لكن بمعرّف آخر رفض الطلب. ويرفضه كذلك إذا كان معرّف المشروع أو البيئة المسجل يدل هنا على شيء آخر، أو كان معرّف التشغيل لبيئة أخرى أو لبيئة حُذفت على هذا التثبيت. الرفض لا يغيّر شيئًا. تصبح مالك العميل الذي ينشئه؛ وإذا كان العميل المسجل موجودًا هنا فعليك أن تكون من مالكيه أصلًا. وتكرار الطلب بعد نجاحه لا يغيّر شيئًا.
ما لا تحمله النسخة، وما تفعله بعد الاستعادة:
- الأعضاء. معرّفات الحسابات تخص تسجيل الدخول في التثبيت القديم، فلا يُضاف أحد. ادعُ الأشخاص من جديد.
- مفاتيح الواجهة ورموز الوصول. أصدر مفاتيح عامة وسرية جديدة وحدّث تطبيقاتك؛ ويحتاج الوكلاء رموز وصول
sbp_جديدة. - أسرار Vault. نسخة البيئة تفريغ لقاعدة بياناتها مع ملفاتها، ولا تتضمن مفتاح Vault الجذري الخاص بالخادم، فلا تُقرأ الأسرار التي فيها على خادم جديد (الوصول إلى قاعدة البيانات). احفظها هناك من جديد.
- مفتاح توقيع JWT. للبيئة الجديدة مفتاحها الخاص. يسجّل المستخدمون الدخول من جديد بكلمات مرورهم القديمة، فالنسخة تحفظها؛ أما الجلسات الحالية وروابط Storage الموقّعة فتتوقف.
- الوصول المباشر إلى قاعدة البيانات، وStudio، وRealtime، وEdge Functions. شغّلها من جديد حيث كنت تستعملها؛ ويأخذ الوصول المباشر كلمة مرور جديدة.
تغطي هذا المسار اختبارات وحدات للفهرس والواجهة البرمجية والأمر، وجُرّب من أوله إلى آخره في 2026-09-25: أُخذت نسختان في آلة افتراضية بينما كانت البيئتان تخدمان الطلبات، ثم استُعيدتا بهذه الأوامر الثلاثة على آلة افتراضية ثانية مثبّتة من الصفر، وسجّل مستخدم من كل بيئة قديمة الدخول بكلمة مروره القديمة عبر مفتاح جديد (الدليل). لم تكن تلك البيئات قد استعملت Studio ولا Realtime ولا الوصول المباشر إلى قاعدة البيانات. ويبقى خطر واحد لم يُجرَّب: البيئة التي استعملت هذه الخدمات لها حسابات دخول تمنحها قاعدة بياناتها صلاحية الوصول، والتثبيت الجديد لا يملكها قبل تشغيل الخدمة، فقد ترفض الاستعادة. شغّل الخدمة للبيئة الجديدة قبل أن تستعيد إليها.
النسخ اليدوية الأخرى
النسخ التالية إجراءات يدوية أقدم تحتاج إلى إشراف. لفهم التصميم وراءها، اقرأ الاستعادة.
نسخة باردة للخادم كله
أبسط نسخة احتياطية هي نسخة باردة لكل شيء، تُؤخذ والخدمات كلها متوقفة:
- أوقف كل شيء: المشرف (
systemctl stop sbarbase.serviceعلى الخادم، أو Ctrl+C معlab/dev.py)، وبيئة التشغيل التي شغّلتها يدويًا (/usr/bin/python3 lab/durable_runtime.py stop)، وهدف الاستعادة إذا نُقلت إليه بيئة (/usr/bin/python3 lab/target_runtime.py stop). - تأكد بالأمر
docker psأن لا حاويةsbarbase-durable-*ولا حاويةsbarbase-restore-*تعمل. - انسخ وحدات تخزين Docker الخاصة بحاويات
sbarbase-durable-*وحاوياتsbarbase-restore-*معًا (اعثر عليها بالأمرdocker inspect)، ومعها مجلدي الحالة الخاصين في نسخة المستودع.lab/و.secrets/. بيانات البيئة المنقولة موجودة فقط على وحدات تخزينsbarbase-restore-*، فنسخ وحدات التخزين الدائمة وحدها يفوّتها. أبقِ النسخة مشفّرة وخارج الخادم؛ في.secrets/كل بيانات الاعتماد المولَّدة. - شغّل المشرف من جديد.
هذه الطريقة تنسخ الخادم كله دفعة واحدة. استعادتها تعيد كل البيئات إلى تلك اللحظة، ولا تستطيع استعادة بيئة واحدة وحدها. لم يُجرَّب هذا الإجراء كاستعادة على خادم نظيف.
بيئة واحدة: تصدير مشفّر
يكتب lab/recovery-export.py حزمة مشفّرة لبيئة واحدة: تفريغ قاعدة بياناتها، وحسابات الدخول المحصورة، والصلاحيات والإعدادات، والملفات مع سماتها الموسّعة، وإعدادات مستأجر Storage، ومفاتيح توقيع الروابط. شغّل بيئة التشغيل المملوكة أولًا (/usr/bin/python3 lab/durable_runtime.py up)، ثم نفّذ /usr/bin/python3 lab/recovery-export.py.
تصدير بيئة واحدة يوقف حاليًا كل البيئات على ذلك المحرك. للحصول على لقطة متسقة، يوقف التصدير Auth وREST ونطاق الإدارة وعملية Storage المشتركة لمكان تشغيل المصدر كله، لا للبيئة المصدَّرة وحدها، ويتركها متوقفة بعد انتهائه. كل التطبيقات على الخادم ودخول لوحة الإدارة تبقى متوقفة حتى تشغّل بيئة التشغيل من جديد. خطط له كنافذة صيانة. يُكتب النص المشفّر ومفتاحه المحفوظ منفصلًا تحت .lab/upstream/ بالصلاحية 0600، ويشير إليهما .lab/upstream/recovery-latest.json.
انتبه إلى ما يلي:
- يختار حاليًا أول بيئة جاهزة من البيئة الثابتة الدائمة في المختبر؛ ليس بعد أمرًا عامًا من نوع "صدّر البيئة X".
- تبقى الحزمة في الذاكرة، وحدّها الأقصى 32 MiB.
- الأمر
lab/cutover-export.pyهو النسخة المسيّجة المستخدمة عند نقل بيئة. يضع البيئة في الصيانة، ويعطّل حسابات دخول خدماتها، ويغلق قاعدة المصدر بعد انتهائه. يترك المصدر مسيّجًا عمدًا، وإعادة فتحه خطوة مطابقة مقصودة. لا تستخدمه أمرًا روتينيًا للنسخ الاحتياطي.
الاستعادة إلى محرك منفصل
مسار الاستعادة سلسلة سكربتات بُنيت على البيئة الثابتة في المختبر:
/usr/bin/python3 lab/recovery-restore-db.py # fresh pinned engine, transactional restore, verification
/usr/bin/python3 lab/recovery-check-services.py # original Auth and REST against the restored database
/usr/bin/python3 lab/recovery-check-storage.py # Storage metadata, files and signing keysواصف الهدف خاص (.lab/upstream/recovery-target.json). تحويل الحركة إلى النسخة المستعادة يمر عبر سجل التوجيه بالأداة lab/target_runtime.py؛ انظر دورة حياة الهدف.
إذا لم يبدأ صباربيز بعد طريق العودة
إصدار Storage يرحّل قاعدة storage_metadata المشتركة. وإذا لم تستطع صورة Storage السابقة أن تبدأ على القاعدة المرحَّلة، يترك طريق العودة الإصدار السابق عاجزًا عن البدء، ويظل مدير الخدمة يعيد تشغيله (systemd كل 10 ثوانٍ، وrestart: unless-stopped في Docker). وكل استعادة تحتاج قاعدة البيانات عاملة. لذلك أوقف صباربيز أولًا: sudo systemctl stop sbarbase، أو docker compose stop sbarbase. بعدها يشغّل الأمر /usr/bin/python3 lab/backup.py start-database حاوية قاعدة البيانات وحدها. شغّله هو والاستعادات التالية بحساب الخدمة، فهو مالك الحالة: بحساب آخر يرفض الأمر، لأن الحالة الخاصة ليست ملك ذلك الحساب (ومع sudo يكون الرفض عند تثبيت الجيل). يرفض ما دام مشرف صباربيز يمسك قفله، لكن الخدمة المتعثرة لا تمسكه إلا وهي تحاول البدء: بين محاولة وأخرى يكون القفل حرًا فيمر الفحص، والاستعادات لا تعيد هذا الفحص، فإعادة تشغيل في منتصفها توقف قاعدة البيانات تحت الاستعادة، ثم تشغّل طبقة التشغيل من جديد على استعادة لم تكتمل؛ ولا شيء عند البدء يفحص ذلك. عندها تتوقف الاستعادة، وما تطبعه يتوقف على الموضع الذي أصابته فيه: رفض عادي، أو recover-restore، أو complete-restore. ولهذا يجب إيقاف الخدمة لا تركها تتعثر. ويرفض كذلك ما دام ترحيل جيل ينتظر، ولا يشغّل إلا الحاوية التي يسمّيها تثبيت الجيل في HBA، بالصورة التي يثبّتها قفل هذا الإصدار، ثم ينتظر حتى تقبل الاتصالات. ولا يشغّل شيئًا غيرها. ولأن طريق العودة ينتهي إلى الإصدار السابق، فالأمر متاح فقط إذا كان في ذلك الإصدار. وفي Docker، والحاوية متوقفة، شغّل هذه الأوامر بالصيغة docker compose run --rm --entrypoint /usr/bin/python3 sbarbase lab/backup.py ... بدل docker compose exec؛ ولم تُجرَّب هذه الصيغة بعد.
بعد ذلك استعد ما رحّله الإصدار من النسخ الاحتياطية المأخوذة قبل الترقية، وstorage_metadata أولًا: restore-storage <backup>، ثم restore <environment> <backup> لكل بيئة. الترتيب مهم هنا، على عكس الإلغاء بعد طريق عودة ناجح: كل استعادة تنتهي بإعادة تشغيل Storage، ولا يبدأ Storage إلا حين تكون storage_metadata هي التي يستطيع العمل عليها، لذلك لا تكتمل استعادة البيئة التي تسبقها، وإكمالها المعلّق يرفض restore-storage وكل استعادة أخرى وdiscard-previous؛ ويفشل complete-restore وrecover-restore بالطريقة نفسها ما دام Storage لا يستطيع البدء. ولا يوجد بعد أمر يتجاوز ذلك، فالتزم بهذا الترتيب. وصباربيز متوقف في أثناء ذلك، فلا يصل شيء إلى Storage بين الاستعادات. ثم شغّل صباربيز. ينجح هذا حين تكون الحاويات الباقية على الصور التي يثبّتها الإصدار الحالي: ترفض الاستعادة حاوية Storage على صورة أخرى، وترفض استعادة البيئة، قبل أن تغيّر شيئًا، إذا كانت حاوية Auth أو REST أو Realtime الخاصة بها على صورة أخرى. ويصح ذلك حين تكون الترقية التي تتراجع عنها قد غيّرت Storage وحده. فكل إصدار من صباربيز يغيّر مكوّنًا أصليًا واحدًا، لكن الترقية الواحدة قد تعبر عدة إصدارات: قناة التحديث تعرض أحدث إصدار يمكن الوصول إليه، وlab/upgrade.py start --to يقبل أي إصدار. والترقية التي عبرت معه تغييرًا في Auth أو REST أو Realtime تترك تلك الحاويات على الصور الأحدث، لأن طريق العودة يتوقف قبل أن يستبدلها، فترفض عندها كل استعادة بيئة؛ وهذه الحالة لا يغطيها هذا القسم. ولكي يبقى التراجع عن إصدار Storage ممكنًا بهذه الطريقة، رقِّ أولًا إلى الإصدار الذي يسبقه مباشرة. ولهذا الإجراء اختبارات وحدات فقط: لم يفشل طريق عودة بهذه الطريقة على مضيف حي بعد.
إذا انقطعت الاستعادة
الاستعادة الفاشلة تترك واصفها خلفها، وإعادة التشغيل العادية ترفض. لا تعدّل تلك الحالة يدويًا:
/usr/bin/python3 lab/recovery_reconcile.py # stop the retained target containers
/usr/bin/python3 lab/retire_recovery_target.py --reason "why" # archive the descriptor, keep containers and volumes
/usr/bin/python3 lab/recovery-restore-db.py # a fresh restore may now runالحدود
- النسخ اليومي والاحتفاظ والنسخ خارج الخادم مشروحة في أول هذه الصفحة. ينطبق عليها من هذه القائمة: لا استعادة إلى لحظة زمنية محددة.
- النسخة الباردة والتصدير المشفّر أعلاه يعنيان توقف كل البيئات على المحرك، لا البيئة المحفوظة وحدها.
- النسخ خارج الخادم يدعم S3 فقط؛ أهداف SSH وrsync غير مبنية. استُعيدت مجموعات التشغيل المشفّرة من Cloudflare R2 على الخادم الحقيقي (بيئة واحدة، بلا بيانات تطبيق، دون انقطاع)؛ أما
lab/offsite.pyفجُرّب على MinIO محلي فقط. ولم تُجرَّب بعد الاستعادة على خادم جديد من النسخة الخارجية. - كل مجموعة تُرفع دفعة واحدة، فالمجموعة الأكبر من حد الرفع الواحد عند المزوّد (5 GiB في Amazon S3) تفشل. تُكتب المجموعة مشفّرة على هذا الخادم قبل رفعها، فيحتاج القرص إلى مساحة لها.
- الاحتفاظ على الهدف بالعدد لا بالعمر.
- مجموعة التشغيل المشفّرة المكتوبة منذ 2026-09-26 تحمل المجلد
storage/، والإصدار الأقدم من ذلك يرفض المجموعة كلها لأن فيها عنصرًا غير متوقع. والمجموعات المكتوبة منذ 2026-10-15 تحمل أيضًا المجلدmanagement/، وكل إصدار أقدم يرفضها بالطريقة نفسها. فبعد العودة إلى إصدار كهذا يفشلoffsite-fetchوrestore --offsiteمع تلك المجموعات؛ أما النسخ المحلية، ومنها المأخوذة قبل الترقية، فلا تتأثر. - الاستعادة تحتاج أن تكون البيئة منشورة على ذلك الخادم. على تثبيت جديد ينشئها
sbarbase relinkأولًا (القسم أعلاه)؛ أما النسخة المأخوذة قبل 2026-09-23 فلا تسجّل الملكية، وتُستعاد إلى بيئة تنشئها بنفسك. أدوار قاعدة البيانات المشتركة على مستوى المحرك كله ليست في المجموعة. - جُرّبت الاستعادة على جهاز واحد مع بيئة اختبار ثابتة، لا على خادم فيه بيانات عملاء حقيقية. ولا تغطي نسخة
storage_metadataوالأمرrestore-storageإلا اختبارات الوحدات؛ لم يُشغَّل أي منهما على Storage حي بعد. والأمر نفسه لنسخةmanagementوالأمرrestore-management، فلم يُشغَّلا على Auth إدارة حي بعد. - بعد أن يقبل الهدف المستعاد عمليات كتابة، تصبح العودة إلى المصدر القديم غير آمنة دون مطابقة.
السجل الكامل مع أعداد الفحوص في الاستعادة المستقلة.
جمع محلي خاص لملفات التثبيت
يجمع الأمر sbarbase backup collect-installation --output /absolute/private/directory الفهرس الأصلي كاملًا ومخزن المفاتيح المُدار، وكل ملف في .secrets/upstream، وملفات الحالة والإعداد بصيغة JSON في المستوى الأعلى، وشيفرة Edge Functions المحفوظة بما فيها مجلدات الإصدارات والملفات المشتركة، وملفات قفل الصور الموجودة. يجب ألا يكون المجلد الهدف موجودًا، وأن يقع خارج التثبيت داخل مجلد خاص تملكه. تُكتب الملفات بالصلاحية 0600، والمجلدات خاصة. أبقِ هذه المجموعة بعيدة عن غيرك: فيها أسرار، وهي غير مشفّرة.
يعلن الأمر local-collection-only وcomplete: false وencrypted: false وrestore_admitted: false. يسجّل ملفها collection.json بصمات المواد في مكان خاص، ويسمّي المصادر الأصلية الناقصة. لا تصلح هذه المجموعة نسخة احتياطية كاملة، ولا حزمة استعادة على خادم جديد، ولا إيصالًا يسمح بالحذف. ولا تُبطل عقود التنفيذ المحفوظة ولا منح MFA ولا بيانات الاعتماد، ولا تفعّل أي مخزن مستعاد.
تحتفظ كل معاملة قراءة للفهرس ولمخزن المفاتيح بلقطة SQLite متسقة من الأصل. ويُرفض النشر إذا تغيّرت هوية ملف إعداد أو ملف آخر، أو تغيّر ما في شجرة المجلدات. لا يضمن ذلك معاملة واحدة تجمع المخزنين، ولا توقّف الكتابة إلى قاعدة البيانات والملفات الأصلية في أثناء الجمع.
الأمر sbarbase backup installation --output /absolute/private/file --key-file /absolute/private/key محجوز للحزمة الكاملة الموثّقة. يرفض حاليًا، لأن جرد الميزات الأصلي الحقيقي، ومصدر مواد توقيع Storage، وقبول الخادم المصدر وموارده، ومصادر قاعدة البيانات والملفات الأصلية، كلها غير موصولة بعد. وما زالت الاستعادة الكاملة على خادم مختلف، مع تعذّر الوصول إلى حالة المصدر، مطلوبة. أما backup now والنسخ خارج الخادم واستعادة البيئة فتحتفظ بنطاقها الحالي.
يستعمل الجامع نقاط الدخول الأصلية المدعومة لـ Edge Functions: index.ts وindex.js وindex.tsx وindex.jsx وindex.mjs. يحلّل مترجم TypeScript المثبّت الإصدار الوحدات الملتقطة، ليتحقق من الاستيرادات وإعادات التصدير المحلية الثابتة، ومنها الاستيرادات المشتركة، مقابل شجرة الإصدار المنشور بالضبط. يُرفض الجمع إذا نقصت وحدة محلية أو ظهر تعبير وحدة ديناميكي غير مدعوم. أما مراجع npm وjsr وHTTPS ووحدات Node البعيدة فتُسجَّل في مكان خاص اعتمادياتٍ خارجية لم يُتحقق منها. ويبقى تشغيل Deno، وتحميل الشيفرة في وقت التشغيل، وتوفر الوحدات الخارجية، رهن فحوص الاستعادة الأصلية اللاحقة. ويُرفض الجمع عند أي خطأ في قراءة مجلد، فلا يسقط مجلد فرعي خاص غير مقروء بصمت. ويُرفض كذلك إذا تغيّرت هوية مجلد أو محتواه.
يحتاج الجمع المحلي Node.js 22.6 أو أحدث، وحزمة TypeScript 7.0.2 المثبّتة من ملف القفل المجمّد للمشروع. يستعمل المساعد وحدات JavaScript قياسية، ولا يحتاج دعم Node لحذف أنواع TypeScript. تعتمد واجهة المترجم البرمجية على مقابض الأنابيب في Node، فلا تعمل تحت Bun. يُفحص إصدار Node واستيرادات المحلّل قبل قراءة أي مادة مصدرية أو تجهيز أي مجموعة، وإذا غابت الأدوات أو كانت غير مدعومة رفض الأمر صراحة. لعملية المحلّل الفرعية وما يتفرع عنها من عمليات المترجم مهلة مشتركة محددة، وتُنظَّف كلها معًا عند الفشل.
تثبّت حاوية طبقة التحكم المدعومة Node من لقطة حزم Ubuntu الثابتة نفسها التي تأتي منها Python، وتتحقق من الحد الأدنى للإصدار في أثناء البناء. وتعلن مهام CI للمصدر Node 22.23.2 صراحة. تحتاج هذه التغييرات فحوص الحاوية وCI الفعلية قبل أي تفعيل في الإنتاج؛ ونجاح الاختبارات المحلية لا يثبت أن حاوية أُعيد بناؤها قد نُشرت.