Mutisite و كائنات Cloudflare R2

استخدام Cloudflare R2 لموقع واحد فقط في تثبيت Discourse متعدد المواقع

إذا كنت تشغّل تثبيت متعدد المواقع (multisite) لـ Discourse وتريد استخدام موقع واحد فقط منها مع Cloudflare R2 (للرفع والنسخ الاحتياطي، وخيارات الأصول الثابتة)، فإن الدليل القياسي تكوين مزوّد تخزين كائنات متوافق مع S3 للرفع لا يغطي تمامًا هذا التعقيد الخاص بالتثبيتات متعددة المواقع. يتناول هذا المنشور ما يحدث فعليًا، وما الذي يبقى محصورًا في كل موقع مقابل ما يكون على مستوى العنقود بأكمله، وبعض الحواف الحادة التي واجهتها والتي كلفتني وقتًا حقيقيًا لفهمها.

الجوهر الذي يجب فهمه: GlobalSetting مقابل SiteSetting

كل شيء في هذا الإعداد يعتمد على تمييز واحد:

  • متغيرات البيئة في app.yml (DISCOURSE_*) تصبح GlobalSettings — تُقرأ مرة واحدة عند إقلاع الحاوية، من بيئة العملية، ومُشارَكة بين جميع المواقع في العنقود. لا يؤثر RAILS_DB عليها.
  • حقول واجهة الإدارة (Admin UI) هي SiteSettings عادية — تُخزَّن لكل موقع في قاعدة بياناته الخاصة، ومحصورة فعليًا في ذلك الموقع فقط.

إذا وُجد GlobalSetting لشيء ما، فإنه يتجاوز SiteSetting المطابق بصمت ويخفي حقله في واجهة الإدارة. هذا يعني: ما تضعه في app.yml ينطبق على كل موقع، بدون استثناءات، وبدون حل بديل عبر RAILS_DB.

الجزء 1 — الرفع والنسخ الاحتياطي (محصور فعليًا في كل موقع، وسهل)

يعمل هذا الجزء تمامًا كما تتوقع. enable_s3_uploads و s3_upload_bucket وbackup_location وs3_backup_bucket وحقول الاعتمادات كلها إعدادات موقع عادية. قم بتكوينها فقط من خلال الإدارة → الإعدادات → ابحث عن “S3”، بعد تسجيل الدخول إلى الموقع المحدد الذي تريده على R2، واترك app.yml دون تغيير. ستواصل المواقع الأخرى في العنقود التخزين محليًا.

قيم نموذجية لـ R2:

Enable S3 uploads = true
Enable direct S3 uploads = true
S3 access key ID / secret access key = <رمز R2 الخاص بك>
S3 region = auto
S3 upload bucket = <اسم الدلو>
S3 endpoint = https://<account-id>.r2.cloudflarestorage.com
S3 CDN URL = https://uploads.yourdomain.com
S3 use ACLs = false   (يستخدم R2 أذونات على مستوى الدلو، وليس ACLs على مستوى الكائن)
S3 backup bucket = <اسم دلو النسخ الاحتياطي>
Backup location = S3

اضبط سياسة CORS لدلوك مباشرةً في لوحة تحكم Cloudflare (لا يحتاج R2 إلى مهمة rake الخاصة بـ CORS في Discourse):

[
  {
    "AllowedOrigins": ["https://your-site.tld"],
    "AllowedMethods": ["GET", "PUT", "POST", "DELETE", "HEAD"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": ["ETag"],
    "MaxAgeSeconds": 3000
  }
]

الجزء 2 — ترحيل الرفع المحلي الموجودة

مهمة rake uploads:migrate_to_s3 تقرأ فقط إعدادات S3 من متغيرات البيئة — ليس لديها أي آلية بديلة لإعدادات الموقع، بغض النظر عما تم تكوينه في واجهة الإدارة. هذه فجوة حقيقية في المهمة، وليست خطأً في التكوين. مرّر الاعتمادات مباشرةً (inline) لتشغيل لمرة واحدة بدلاً من لمس app.yml:

./launcher enter app

RAILS_DB=default \
DISCOURSE_S3_REGION=auto \
DISCOURSE_S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com \
DISCOURSE_S3_BUCKET=<اسم الدلو> \
DISCOURSE_S3_ACCESS_KEY_ID=<المفتاح> \
DISCOURSE_S3_SECRET_ACCESS_KEY=<السر> \
rake uploads:migrate_to_s3

هذه متغيرات البيئة موجودة فقط لتلك العملية (shell process) — لا شيء يبقى بعد خروجك.

خطأ في التلخيص (Checksum) في إصدارات AWS SDK الأحدث

إذا واجهت:

Aws::S3::Errors::InvalidRequest: You can only specify one non-default checksum at a time.

فهذه عدم توافق معروف بين إصدارات aws-sdk-core الحديثة (التي ترسل تلخيص CRC32 افتراضيًا) وR2. قم بالإصلاح بإضافة متغيري بيئة آخرين إلى نفس الأمر:

export AWS_REQUEST_CHECKSUM_CALCULATION=when_required
export AWS_RESPONSE_CHECKSUM_VALIDATION=when_required

سجلات “غير مُرحَّلة” متبقية بعد تشغيل ناجح في الغالب

إذا انتهت المهمة بشيء مثل 1 of 1291 uploads are not migrated، فلا تقلق — كل شيء آخر تم ترحيله بالفعل وتمت إعادة كتابة عناوين URL في قاعدة البيانات. ابحث عن المتأخر في rails c:

base_url = File.join(SiteSetting.Upload.s3_base_url, "original/")
Upload.by_users.where("url NOT LIKE '#{base_url}%'").pluck(:id, :url, :original_filename)

في حالتي، كان هناك سجل Upload ضال (ملف zip لسجل النسخ الاحتياطي) يستخدم عنوان endpoint الخام لـ R2 بدلاً من صيغة عنوان URL الخاص بـ CDN التي يتوقعها الفحص — وهو إيجابي زائف، وليس فشلًا حقيقيًا. صحح عنوان URL أو احذف السجل إذا لم يكن محتوى ذا معنى.

الجزء 3 — الأصول الثابتة (JS/CSS) — الجزء الذي يكون على مستوى العنقود بأكمله

هنا يصطدم هدف “لموقع واحد فقط” بجدار صلب. الأصول المُجمَّعة مُشارَكة عبر العنقود متعدد المواقع بأكمله — هناك حزمة JS/CSS مجمعة واحدة، وليست حزمة لكل موقع. سواء تم تقديم الأصول من R2 أو محليًا يتم حسمه مرة واحدة، عند إقلاع Rails، عبر GlobalSetting.use_s3? — لا يوجد تجاوز لكل موقع لهذا.

إذا كنت تريد تفريغ الأصول إلى R2، فيجب عليك وضع تفاصيل الاتصال (وليس علم تفعيل الرفع) في app.yml:

env:
  DISCOURSE_USE_S3: true
  DISCOURSE_S3_REGION: auto
  DISCOURSE_S3_ENDPOINT: https://<account-id>.r2.cloudflarestorage.com
  DISCOURSE_S3_ACCESS_KEY_ID: "xxx"
  DISCOURSE_S3_SECRET_ACCESS_KEY: "xxx"
  DISCOURSE_S3_BUCKET: <اسم الدلو>
  DISCOURSE_S3_CDN_URL: https://uploads.yourdomain.com
  AWS_REQUEST_CHECKSUM_CALCULATION: when_required
  AWS_RESPONSE_CHECKSUM_VALIDATION: when_required

hooks:
  after_assets_precompile:
    - exec:
        cd: $home
        cmd:
          - sudo -E -H -u discourse bundle exec rake s3:upload_assets
          - sudo -E -H -u discourse bundle exec rake s3:expire_missing_assets

ملاحظات:

  • لا تضبط DISCOURSE_CDN_URL. اضبط فقط DISCOURSE_S3_CDN_URL. ضبط كلاهما، مع تمثيل نطاقك الرئيسي عبر Cloudflare، يسبب حلقات إعادة توجيه (redirect loops) وفقًا لتحذير دليل S3 الرئيسي نفسه.
  • استخدم bundle exec rake، وليس bundle rake (خطأ إملائي سهل) — واستخدم sudo -E -H -u discourse (العلامة -H تضبط HOME بشكل صحيح لمستخدم discourse؛ بدونها، يعود Bundler إلى مجلد مؤقت في كل تشغيل).
  • يؤثر تفريغ الأصول على كلا الموقعين. ستبدأ وسوم <script>/<link> في موقعك الثاني أيضًا في الحل إلى عنوان CDN الخاص بـ R2، لأنها نفس الحزمة المجمعة. تأكد من أن AllowedOrigins في CORS لدلوك يتضمن نطاق كل موقع.
  • هذا لا يجبر الرفع الفعلية لموقعك الثاني على S3 — enable_s3_uploads تظل إعدادًا محصورًا فعليًا في كل موقع، ومستقلًا عن GlobalSetting الخاص بتقديم الأصول. تحقق باستخدام SiteSetting.Upload.enable_s3_uploads في rails c لقاعدة بيانات ذلك الموقع بعد إعادة البناء.

الجزء 3 — USE_DB_S3_CONFIG — ما يفعله فعليًا (وما لا يفعله)

ستجد USE_DB_S3_CONFIG=true مُشارَكًا في بعض إعدادات المجتمع (مثل مخطط Bitnami) كطريقة لجعل s3:upload_assets يقرأ الاعتمادات من إعدادات الموقع بدلاً من متغيرات البيئة. يعمل هذا للمهمة نفسها — لكنه لا يقلب GlobalSetting.use_s3?، وهو العلم الذي يتحكم فعليًا في ما إذا كانت عناوين URL للأصول ستُعاد كتابتها إلى CDN عند العرض. لذا يمكنك دفع الملفات إلى R2 بنجاح باستخدام USE_DB_S3_CONFIG وما زال ترى موقعك يقدم الأصول محليًا، لأن فحص عرض الصفحة لا يرى أبدًا أن “S3 مفعّل”. إذا كنت تريد تقديم الأصول فعليًا من R2، فأنت بحاجة إلى DISCOURSE_USE_S3: true الحقيقي + متغيرات البيئة للاتصال في app.yml، وليس مجرد حل بديل إعداد قاعدة البيانات.

الجزء 4 — ما الذي لن يكون على R2، ولماذا

حتى مع عمل الخطاف (hook)، فإن s3:upload_assets يرفع فقط ما هو موجود في Rails.application.assets.load_path — قائمة Sprockets الخاصة بـ Rails. هناك ثلاث فئات يتم توليدها خارج تلك القناة ولا تظهر أبدًا في هذه القائمة، لذا تبقى على القرص المحلي مهما حدث:

  • CSS الخاص بالسمة (Theme) — يُجمَّع ديناميكيًا لكل سمة/مخطط ألوان بواسطة Stylesheet::Manager في Discourse، وليس عبر Sprockets.
  • theme-javascripts — JS مُجمَّع لكل سمة من ThemeJavascriptCompiler.
  • extra-locale / ملفات JS المحلية — يتم توليدها بواسطة JsLocaleHelper.

هذا ليس مشكلة في التكوين — هذه لم تكن أصول Sprockets أصلًا، لذا لا يوجد متغير بيئة يجلبها. عمليًا، هذا يعني: حزمة JS الأساسية/للموردين (vendor) → تم تفريغها إلى R2 بنجاح؛ CSS/JS الخاص بالسمة واللغات المحلية → تبقى محلية، وتقدمها التطبيق مباشرة. هذا حالة طبيعية وعاملة، وليست حالة معطلة.

ملخص: ما الذي يجب وضعه أين

ما أين النطاق
enable_s3_uploads، s3_upload_bucket، backup_location، s3_backup_bucket واجهة الإدارة، لكل موقع لكل موقع
الاعتمادات + DISCOURSE_S3_REGION/ENDPOINT/BUCKET/CDN_URL + DISCOURSE_USE_S3 app.yml، فقط إذا كنت تريد تفريغ CDN للأصول على مستوى العنقود (لا مفر منه)
خطاف after_assets_precompile app.yml على مستوى العنقود
AWS_REQUEST_CHECKSUM_CALCULATION / AWS_RESPONSE_CHECKSUM_VALIDATION app.yml على مستوى العنقود (علم سلوك SDK غير ضار)
CORS على الدلو لوحة تحكم Cloudflare يجب أن تتضمن نطاق كل موقع إذا كانت الأصول مشتركة

إذا لم تكن بحاجة إلى تفريغ CDN للأصول، فتخطَّ الجزء 3 بالكامل — يمكنك تشغيل إعداد R2 يعمل بالكامل ومحصور فعليًا في كل موقع (الرفع + النسخ الاحتياطي فقط) دون لمس app.yml أبدًا.

إعجاب واحد (1)