نظرة عامة على Active Storage في ريلز
يغطّي هذا الدليل كيفيّة إرفاق ملفات بنماذج Active Record. ستتعلم بعد قراءة هذا الدليل:
- كيفية إرفاق ملف أو عدّة ملفّات بسجل (record).
- كيفيّة حذف ملف مُرفق.
- كيفيّة الربط بملف مُرفق.
- كيفيّة استخدام المتغيّرات (variants) لتحويل الصور.
- كيفيّة إنشاء تمثيل صُوَري (image representation) لملف غير صُوَري، مثل ملف PDF أو فيديو.
- كيفيّة إرسال تحميلات الملفّات مباشرةً من المتصفّحات إلى خدمة تخزين، دون المرور على خوادم تطبيقك.
- كيفيّة تنظيف الملفّات المخزّنة أثناء الاختبار.
- كيفيّة تعريف استخدام (implement) الدعم لخدمات تخزين إضافيّة.
ما هو Active Storage؟
يُسهّل Active Storage تحميل الملفّات إلى خدمة تخزين سحابية (cloud storage) مثل Amazon S3 أو Google Cloud Storage أو Microsoft Azure Storage وإرفاق هذه الملفّات بكائنات Active Storage. يأتي مع خدمات التخزين تلك خدمة تستند على أقراص تخزين محلية للتطوير والاختبار، كما تدعم نسخ الملفّات إلى خدمات تابعة (subordinate services) للنسخ الإحتياطي والتهجيرات.
باستخدام Active Storage، يمكن للتطبيق تحويل تحميلات الصور باستخدام ImageMagick، وإنشاء صور تمثيلية للتحميلات غير الصُوريّة مثل ملفّات PDF ومقاطع الفيديو، واستخراج البيانات الوصفيّة (metadata) من ملفّات عشوائيّة.
الإعداد
يستخدم Active Storage جدولين في قاعدة بيانات تطبيقك المسمّيَين active_storage_blobs
و active_storage_attachments
. بعد إنشاء تطبيق جديد (أو ترقية تطبيقك إلى الإصدار 5.2 من ريلز)، نفِّذ الأمر rails active_storage:install
لإنشاء عملية تهجير تُنشئ هذين الجدولين. استخدم rails db:migrate
لتشغيل التهجير.
صرّح عن خدمات Active Storage في config/storage.yml. قدّم اسمًا مع التهيئة المطلوبة لكل خدمة يستخدمها تطبيقك. يوضح المثال أدناه ثلاث خدمات تدعى local و test و amazon:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
أخبر Active Storage بأي خدمة تريد استخدامها من خلال تعيين Rails.application.config.active_storage.service
. من المستحسن فعل هذا مع كل بيئة نظرًا لأن من المحتمل أنّ كلّ بيئة تستخدم خدمة مختلفة. لاستخدام خدمة القرص (disk service) من المثال السابق في بيئة التطوير، يمكنك إضافة ما يلي إلى الملف config/environments/development.rb:
# تخزين الملفات محليًّا
config.active_storage.service = :local
لاستخدام خدمة Amazon S3 في الإنتاج، يمكنك إضافة ما يلي إلى الملف config/environments/production.rb:
# Amazon S3 تخزين الملفات على
config.active_storage.service = :amazon
استمر في القراءة لمزيد من المعلومات حول مُهيئات الخدمة المضمّنة (مثل Disk و S3) والتهيئة التي تتطلّبها.
خدمة القرص
صرّح بتعريف خدمة قرص في config/storage.yml:
local:
service: Disk
root: <%= Rails.root.join("storage") %>
خدمة Amazon S3
صرّح بتعريف خدمة S3 في config/storage.yml:
amazon:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
أضف الجوهرة aws-sdk-s3 إلى Gemfile:
gem "aws-sdk-s3", require: false
ملاحظة: تتطلّب ميزات Active Storage الأساسيّة الأذونات (permissions) التالية: s3:ListBucket
و s3:PutObject
و s3:GetObject
و s3:DeleteObject
. قد تحتاج إلى أذونات إضافية إن كانت لديك خيارات تحميل إضافية مُهيّئة مثل إعداد قوائم التحكم في الوصول (ACL).
ملاحظة: إن أردت استخدام متغيّرات البيئة أو ملفّات ضبط SDK القياسية، أو الملفّات الشخصية، أو ملفّات تعريف نُسخ IAM أو أدوار المهام (task roles)، يمكنك تجاهل access_key_id
و secret_access_key
، ومفاتيح region
في المثال أعلاه. تدعم خدمة Amazon S3 جميع خيارات الاستيثاق (authentication) الموضّحة في توثيق AWS SDK.
خدمة Microsoft Azure Storage
صرّح بتعريف خدمة Azure Storage في config/storage.yml:
azure:
service: AzureStorage
storage_account_name: ""
storage_access_key: ""
container: ""
أضف الجوهرة azure-storage إلى Gemfile:
gem "azure-storage", require: false
خدمة التخزين السحابي من Google
صرّح عن خدمة التخزين السحابي من Google في config/storage.yml:
google:
service: GCS
credentials: <%= Rails.root.join("path/to/keyfile.json") %>
project: ""
bucket: ""
يمكنك اختياريًّا تزويد جدول Hash من بيانات الاعتماد بدلًا من مسار ملف مفاتيح (keyfile path):
google:
service: GCS
credentials:
type: "service_account"
project_id: ""
private_key_id: <%= Rails.application.credentials.dig(:gcs, :private_key_id) %>
private_key: <%= Rails.application.credentials.dig(:gcs, :private_key).dump %>
client_email: ""
client_id: ""
auth_uri: "https://accounts.google.com/o/oauth2/auth"
token_uri: "https://accounts.google.com/o/oauth2/token"
auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs"
client_x509_cert_url: ""
project: ""
bucket: ""
أضف الجوهرة google-cloud-storage إلى Gemfile:
gem "google-cloud-storage", "~> 1.8", require: false
خدمة التماثل الانعكاسي (Mirror Service)
يمكنك الاحتفاظ بعدّة خدمات متزامنة من خلال تعريف خدمة التماثل الانعكاسي (Mirror Service). عندما يُحمّل ملف أو يُحذف، يُطبّق الفعل على جميع الخدمات المشتركة في التماثل الانعكاسي. يمكنك البدء في انعكاس (mirroring) خدمة جديدة، ونسخ الملفّات الموجودة من الخدمة القديمة للجديدة، ثم أخذ كامل راحتك مع النسخة الجديدة. عرّف كل الخدمات التي ترغب في استخدامها كما هو موضّح أعلاه وأشر إليها من إحدى الخدمات المُنعكسة (mirrored service).
s3_west_coast:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
s3_east_coast:
service: S3
access_key_id: ""
secret_access_key: ""
region: ""
bucket: ""
production:
service: Mirror
primary: s3_east_coast
mirrors:
- s3_west_coast
ملاحظة: تُخدَّم الملفّات من الخدمة الأساسيّة (primary service).
ملاحظة: هذا غير متوافق مع ميزة التحميلات المباشرة.
إرفاق ملفات بالسجلات
has_one_attached
يُعدّ الماكرو has_one_attached
رابطًا مباشرًا (one-to-one mapping) بين السجلّات والملفّات. يمكن أن يُرفق كل سجل بملف واحد فقط.
فلنفترض على سبيل المثال أنّ تطبيقك يحتوي على النموذج User
. إن أردت أن يكون لكل مستخدم صورة شخصيّة، عرّف النموذج User
كما يلي:
class User < ApplicationRecord
has_one_attached :avatar
end
يمكنك إنشاء مستخدم مع صورة رمزيّة (avatar):
class SignupController < ApplicationController
def create
user = User.create!(user_params)
session[:user_id] = user.id
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:email_address, :password, :avatar)
end
end
استدع avatar.attach
لإرفاق صورة شخصيّة لمُستخدم حالي:
Current.user.avatar.attach(params[:avatar])
استدع ?avatar.attached
لتحديد ما إن امتلك لمستخدم معيّن صورة شخصية أم لا:
Current.user.avatar.attached?
has_many_attached
يُعدُّ الماكرو has_many_attached
علاقة واحد لمتعدد (one-to-many relationship) بين السجلّات والملفّات. يمكن أن يُرفق بكل سجل العديد من الملفّات.
فلنفترض على سبيل المثال أنّ تطبيقك يحتوي على النموذج Message
. إن أردت أن تحتوي كل رسالة على العديد من الصور، عرّف النموذج Message
كما يلي:
class Message < ApplicationRecord
has_many_attached :images
end
يمكنك إنشاء رسالة تحتوي على صور:
class MessagesController < ApplicationController
def create
message = Message.create!(message_params)
redirect_to message
end
private
def message_params
params.require(:message).permit(:title, :content, images: [])
end
end
استدع images.attach
لإضافة صور جديدة لرسالة موجودة:
@message.images.attach(params[:images])
استدع ?images.attached
لتحديد ما إن امتلكت رسالة محدَّدة أية صورة:
@message.images.attached?
إرفاق ملفات / كائنات IO
تحتاج أحيانًا لإرفاق ملف لا يصل عبر طلب HTTP. قد ترغب على سبيل المثال في إرفاق ملف أنشأته على القرص أو نزّلته من عنوان URL أرسله المستخدم. قد ترغب أيضًا في إرفاق ملف تجريبي (fixture file) في نموذج اختبار. للقيام بذلك، وفّر كائن Hash
تحتوي على الأقل على كائن IO
مفتوح واسم ملف:
@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf')
اكتب عند الإمكان نوع المحتوى أيضًا. يحاول Active Storage تحديد نوع محتوى الملف من بياناته. يعود إلى نوع المحتوى الذي قدّمته إن عجز عن ذلك.
@message.image.attach(io: File.open('/path/to/file'), filename: 'file.pdf', content_type: 'application/pdf')
إن لم تقدّم نوع محتوى ولم يستطع Active Storage من تحديد نوع محتوى الملف تلقائيًا، فسيعيَّن افتراضيًا إلى application/octet-stream.
حذف الملفات
استدع purge
على الملفّات المُرفقة لإزالتها من النماذج. يمكن إجراء الإزالة في الخلفيّة إن أُعدّ تطبيقك لاستخدام Active Job. تزيل عملية الحذف (Purging) كائن البيانات الثنائية (The Blob) والملف من خدمة التخزين.
# حذف الصورة الرمزية والملفات المرجعية الفعلية
ser.avatar.purge
# حذف النماذج المرتبطة، والملفات المرجعية الفعلية عبر الوظيفة الفعالة
user.avatar.purge_later
إنشاء روابط للملفات
أنشئ عنوان URL دائم لكائن البيانات الثنائية (blob) الذي يشير إلى التطبيق. يُعاد التوجيه عند الوصول إلى نقطة نهاية الخدمة (service endpoint) الفعليّة. تفصل إعادة التوجيه عنوان URL العام عن العنوان الفعلي ويسمح على سبيل المثال بعكس صور المُرفقات في خدمات مختلفة لتكون متوافرة دومًا. تنتهي الصلاحيّة HTTP لإعادة التوجيه بعد 5 دقائق.
url_for(user.avatar)
لإنشاء رابط تنزيل، استخدم rails_blob_{path|url}
. يتيح لك استخدام هذا المساعد ضبط الترتيب (disposition).
rails_blob_path(user.avatar, disposition: "attachment")
إن إحتجت لإنشاء رابط من خارج سياق المتحكم/العرض (مهام الخلفية، Cronjobs، ...إلخ.) يمكنك الوصول إلى rails_blob_path
كما يلي:
Rails.application.routes.url_helpers.rails_blob_path(user.avatar, only_path: true)
تنزيل الملفات
إن إحتجت لمعالجة كائنات البيانات الثنائية (blobs) من جانب الخادم مثل عند إجراء تحليل أو المزيد من التحويلات، يمكنك تنزيلها والحصول على البيانات الثنائية:
binary = user.avatar.download
قد ترغب في بعض الحالات بتحويله إلى ملف فعلي على القرص لتمرير مسار الملف إلى برامج خارجية (مثل برامج مكافحة الفيروسات، أو المحولات، أو المحسّنات ، أو المُصغّرات [minifiers]، ...إلخ). يمكنك في هذه الحالة تضمين الوحدة ActiveStorage::Downloading
في صنفك والتي توفّر مساعدين لتنزيل الملفّات مباشرة مع تجنب تخزين الملف في الذاكرة. يتوقع ActiveStorage::Downloading
تعريف تابع يدعى blob
.
class VirusScanner
include ActiveStorage::Downloading
attr_reader :blob
def initialize(blob)
@blob = blob
end
def scan
download_blob_to_tempfile do |file|
system 'scan_virus', file.path
end
end
end
يُنشئ download_blob_to_tempfile
ملفّات في Dir.tmpdir
بشكل افتراضي. إن احتجت لاستخدام مُجلّد آخر، أعد تعريف ActiveStorage::Downloading#tempdir
في صنفك:
class VirusScanner
include ActiveStorage::Downloading
# ...
private
def tempdir
'/path/to/tmp'
end
end
قد ترغب أيضًا في استخدام chmod
على الملف ومجلّده إن شُغّل البرنامج الخارجي كبرنامج منفصل، حيث يتعذّر على المستخدمين الآخرين الوصول إليه لأن Tempfile سيُعيّن الأذونات إلى النمط 0600.
تحويل الصور
لإنشاء شكل مختلف للصورة، استدع variant
على كائن البيانات الثنائية (the Bolb). يمكنك تمرير أي تحويل MiniMagick معتمد من قبل التابع.
أضف mini_magick
إلى ملفك Gemfile لتفعيل المتغيرات:
gem 'mini_magick'
عندما يضغط المتصفح على العنوان URL البديل، يُحوّل Active Storage كائن البيانات الثنائي الأصلي إلى الصيغة (format) المحدّدة ويعيد توجيهه إلى موقع خدمته الجديد.
<%= image_tag user.avatar.variant(resize: "100x100") %>
معاينة الملفات (Previewing Files)
يمكن معاينة بعض الملفّات غير الصُوريّة: أي يمكن تقديمها كصور. يمكن على سبيل المثال معاينة ملف فيديو عن طريق استخراج أول إطار له. يدعم Active Storage معاينة مقاطع الفيديو ومستندات PDF مباشرة.
<ul>
<% @message.files.each do |file| %>
<li>
<%= image_tag file.preview(resize: "100x100>") %>
</li>
<% end %>
</ul>
تحذير: يتطلّب استخراج المعاينات تطبيقات تابعة لجهات خارجية و ffmpeg
للفيديو و mutool
بالنسبة الى PDF. لا يُوفّر ريلز هذه المكتبات. عليك تثبيتها بنفسك لاستخدام المُعايِنات المضمّنة (built-in previewers). احرض على فهمك للتأثيرات المترتبة على ترخيص (licensing) تطبيقك قبل تثبيت برامج الجهات الخارجية واستخدامها.
التحميل المباشر
يدعم Active Storage، مع مكتبة JavaScript المضمنة ضمنه، التحميل مباشرة من العميل إلى السحابة.
تثبيت التحميل المباشر
1. ضمّن activestorage.js في حزمة JavaScript تطبيقك.
- باستخدام خط أنابيب الأصول (asset pipeline):
//= require activestorage
- باستخدام حزمة npm:
import * as ActiveStorage from "activestorage"
ActiveStorage.start()
2. علّق على مُدخلات الملف باستخدام عنوان URL للتحميل المباشر.
<%= form.file_field :attachments, multiple: true, direct_upload: true %>
3. انتهت العمليّة! تبدأ عمليّات التحميل عند إرسال الإستمارات (form).
أحداث تحميل JavaScript المباشرة
اسم الحدث | هدف الحدث | بيانات الحدث (event.detail) | وصف |
---|---|---|---|
direct-uploads:start
|
<form>
|
None | أُرسلت استمارة تحتوي على ملفّات لحقول التحميل المباشر. |
direct-upload:initialize
|
<input>
|
{id, file}
|
يُطلق لكل ملف بعد إرسال الاستمارة. |
direct-upload:start
|
<input>
|
{id, file}
|
بدءَ تحميل مباشر. |
direct-upload:before-blob-request
|
<input>
|
{id, file, xhr}
|
قبل تقديم طلب إلى تطبيقك للبيانات الوصفية للتحميل المباشر. |
direct-upload:before-storage-request
|
<input>
|
{id, file, xhr}
|
قبل تقديم طلب لتخزين ملف. |
direct-upload:progress
|
<input>
|
{id, file, progress}
|
بينما تتقدّم طلبات تخزين الملفّات. |
direct-upload:error
|
<input>
|
{id, file, error}
|
حدث خطأ. سيُعرض خطأ ما لم يُلغ هذا الحدث. |
direct-upload:end
|
<input>
|
{id, file}
|
انتهت عملية تحميل مباشر. |
direct-uploads:end
|
<form>
|
None | انتهت جميع التحميلات المباشرة. |
مثال
يمكنك استخدام هذه الأحداث لإظهار تقدّم عملية التحميل.
لإظهار الملفّات المُحمّلة في استمارة:
// direct_uploads.js
addEventListener("direct-upload:initialize", event => {
const { target, detail } = event
const { id, file } = detail
target.insertAdjacentHTML("beforebegin", `
<div id="direct-upload-${id}" class="direct-upload direct-upload--pending">
<div id="direct-upload-progress-${id}" class="direct-upload__progress" style="width: 0%"></div>
<span class="direct-upload__filename">${file.name}</span>
</div>
`)
})
addEventListener("direct-upload:start", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.remove("direct-upload--pending")
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail
const progressElement = document.getElementById(`direct-upload-progress-${id}`)
progressElement.style.width = `${progress}%`
})
addEventListener("direct-upload:error", event => {
event.preventDefault()
const { id, error } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--error")
element.setAttribute("title", error)
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail
const element = document.getElementById(`direct-upload-${id}`)
element.classList.add("direct-upload--complete")
})
إضافة التنسيقات (styles):
/* direct_uploads.css */
.direct-upload {
display: inline-block;
position: relative;
padding: 2px 4px;
margin: 0 3px 3px 0;
border: 1px solid rgba(0, 0, 0, 0.3);
border-radius: 3px;
font-size: 11px;
line-height: 13px;
}
.direct-upload--pending {
opacity: 0.6;
}
.direct-upload__progress {
position: absolute;
top: 0;
left: 0;
bottom: 0;
opacity: 0.2;
background: #0076ff;
transition: width 120ms ease-out, opacity 60ms 60ms ease-in;
transform: translate3d(0, 0, 0);
}
.direct-upload--complete .direct-upload__progress {
opacity: 0.4;
}
.direct-upload--error {
border-color: red;
}
input[type=file][data-direct-upload-url][disabled] {
display: none;
}
الدمج مع مكتبات أو أطر عمل
إن رغبت في استخدام ميزة "التحميل المباشر" (Direct Upload) من إطار عمل JavaScript أو إن أردت حلولًا أخرى مثل السحب والإفلات (drag and drop)، تستطيع استخدام الصنف DirectUpload
لهذا الغرض. عند استلام ملف من مكتبتك المختارة، أنسخ (instantiate) نسخة DirectUpload
واستدع تابعها create
. يطلب create
رد نداء يستدعى (invoke) عند اكتمال التحميل.
import { DirectUpload } from "activestorage"
const input = document.querySelector('input[type=file]')
// على عنصر أب أو onDrop استخدم file drop أربط ب
// Dropzone استخدم مكتبة مثل
const onDrop = (event) => {
event.preventDefault()
const files = event.dataTransfer.files;
Array.from(files).forEach(file => uploadFile(file))
}
// أربط بعملية انتقاء ملف عاديّة
input.addEventListener('change', (event) => {
Array.from(input.files).forEach(file => uploadFile(file))
// تستطيع تفريغ الملفات المنتقاة من المُدخلات
input.value = null
})
const uploadFile = (file) {
// والذي file_field direct_upload: true تحتاج استمارتك
// data-direct-upload-url يوفّر
const url = input.dataset.directUploadUrl
const upload = new DirectUpload(file, url)
upload.create((error, blob) => {
if (error) {
// عالج الخطأ
} else {
// أضف مُدخلًا مخفيًّا ذو اسم مناسب للاستمارة مع
// (blob) النقط ids كي تُبث blob.signed_id قيمة
// مع تيّار التحميل العادي
const hiddenField = document.createElement('input')
hiddenField.setAttribute("type", "hidden");
hiddenField.setAttribute("value", blob.signed_id);
hiddenField.name = input.name
document.querySelector('form').appendChild(hiddenField)
}
})
}
تستطيع إن إحتجت لتتبع تقدّم عمليّة تحميل الملف تمرير معامل (parameter) ثالث إلى الدالّة DirectUpload
البانية (constructor). ستستدعي DirectUpload
تابع الكائن directUploadWillStoreFileWithXHR
أثناء التحميل. تستطيع بعد ذلك ربط معالج تقدّمك على XHR.
import { DirectUpload } from "activestorage"
class Uploader {
constructor(file, url) {
this.upload = new DirectUpload(this.file, this.url, this)
}
upload(file) {
this.upload.create((error, blob) => {
if (error) {
// عالج الخطأ
} else {
// أضف حقل إدخال مخفي باسم مناسب إلى النموذج
// blob.signed_id مع تعيين قيمته إلى
}
})
}
directUploadWillStoreFileWithXHR(request) {
request.upload.addEventListener("progress",
event => this.directUploadDidProgress(event))
}
directUploadDidProgress(event) {
// لتحديث شريط التقدم event.loaded و event.total استعمل
}
}
التخلص من الملفات المخزنة خلال اختبارات النظام
تنظّف اختبارات النظام بيانات الاختبار عبر تنفيذ عملية تراجع عكسية (rolling back). ولكن بما أن destroy
لم يستدعَ قط، لا تُنظّف الملفّات المُرفقة. إن رغبت بمسح الملفّات، يمكنك ذلك في رد النداء after_teardown
. ويضمن فعل هذا هنا التأكد من اكتمال كل الاتصالات المُنشئة أثناء الاختبار وعدم تلقي رسالة خطأ من Active Storage تفيد عدم إمكانية العثور على ملف.
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
def remove_uploaded_files
FileUtils.rm_rf("#{Rails.root}/storage_test")
end
def after_teardown
super
remove_uploaded_files
end
end
إن تحقّقت اختبارات نظامك من حذف نموذج يحتوي مُرفقات وكنت تستخدم Active Job، اضبط بيئة اختبارك كي تستخدم محوّل قائمة الانتظار المضمنة (inline queue adapter) بحيث تُنفّذ مهمة التنظيف مباشرةً بدلًا من وقت غير معروف في المستقبل. قد ترغب أيضًا في استخدام تعريف خدمة منفصل لبيئة الاختبار كي لا تَحذف الاختبارات الملفّات التي تُنشئها أثناء التطوير.
# لتنفيذ أشياء مباشرةً (inline) استعمل وظيفة معالجة ضمنية
config.active_job.queue_adapter = :inline
# افصل ملف التخزين في بيئة الاختبار
config.active_storage.service = :local_test
تعريف استخدام الدعم لخدمات سحابية أخرى
ستحتاج لتعريف استخدام الخدمة إن إحتجت إلى دعم خدمة سحابية غير الخدمات المذكورة في هذا الدليل. توسع كل خدمة ActiveStorage::Service
من خلال تعريف استخدام التوابع الضروريّة لتحميل وتنزيل الملفّات من وإلى السحابة.