บทความนี้เป็นส่วนหนึ่งของชุดบทความ "Uploading with Rails" วันนี้เราจะมาทำความรู้จัก CarrierWave หนึ่งในโซลูชันอัปโหลดไฟล์ที่นิยมที่สุดสำหรับ Rails ผมชอบ CarrierWave เพราะเริ่มต้นใช้งานง่าย มีฟีเจอร์มากมายพร้อมใช้งานทันที และมีบทความ "how-to" จากชุมชนมากมาย ทำให้คุณไม่หลงทาง
ในบทความนี้ คุณจะได้เรียนรู้วิธี:
- ผสานรวม CarrierWave เข้ากับแอป Rails
- เพิ่มการตรวจสอบความถูกต้อง (Validations)
- เก็บไฟล์ไว้ข้ามคำขอ (Persist files across requests)
- ลบไฟล์
- สร้างภาพขนาดย่อ (Thumbnails)
- อัปโหลดไฟล์จากที่ตั้งระยะไกล (Remote URLs)
- รองรับการอัปโหลดหลายไฟล์ (Multiple file uploads)
- เพิ่มการรองรับที่เก็บข้อมูลบนคลาวด์ (Cloud storage)
ซอร์สโค้ดสำหรับบทความนี้พร้อมใช้งานบน GitHub อ่านให้สนุกนะครับ!
สร้างโครงพื้นฐาน
เริ่มต้นด้วยการสร้างแอปพลิเคชัน Rails ใหม่:
rails new UploadingWithCarrierwave -T
สำหรับเดโมนี้จะใช้ Rails 5.0.2 โปรดทราบว่า CarrierWave 1 รองรับเฉพาะ Rails 4+ และ Ruby 2 เท่านั้น หากยังใช้ Rails 3 ให้ใช้ CarrierWave เวอร์ชัน 0.11 แทน
เพื่อดู CarrierWave ทำงาน เราจะสร้างบล็อกง่ายๆ ที่มีโมเดล Post เพียงตัวเดียว โดยมีแอตทริบิวต์หลักดังนี้:
title(string)body(text)image(string) — ฟิลด์นี้จะเก็บชื่อไฟล์รูปภาพที่ผูกกับโพสต์
สร้างและรันไมเกรชัน:
rails g model Post title:string body:text image:string rails db:migrate
ตั้งค่าเส้นทาง (Routes):
config/routes.rb
resources :posts root to: 'posts#index'
สร้างคอนโทรลเลอร์พื้นฐาน:
posts_controller.rb
class PostsController < ApplicationController
before_action :set_post, only: [:show, :edit, :update]
def index
@posts = Post.order('created_at DESC')
end
def show
end
def new
@post = Post.new
end
def create
@post = Post.new(post_params)
if @post.save
redirect_to posts_path
else
render :new
end
end
def edit
end
def update
if @post.update_attributes(post_params)
redirect_to post_path(@post)
else
render :edit
end
end
private
def post_params
params.require(:post).permit(:title, :body, :image)
end
def set_post
@post = Post.find(params[:id])
end
end
สร้างวิว index:
views/posts/index.html.erb
<h1>Posts</h1> <%= link_to 'Add post', new_post_path %> <%= render @posts %>
และพาร์เชียลที่เกี่ยวข้อง:
views/posts/_post.html.erb
<h2><%= link_to post.title, post_path(post) %></h2> <p><%= truncate(post.body, length: 150) %></p> <p><%= link_to 'Edit', edit_post_path(post) %></p> <hr>
ที่นี่ใช้เมธอด truncate ของ Rails เพื่อแสดงแค่ 150 ตัวอักษรแรกของโพสต์ ก่อนที่จะสร้างวิวและพาร์เชียลฟอร์มอื่นๆ เรามาผสาน CarrierWave เข้ากับแอปพลิเคชันกันก่อน
ผสานรวม CarrierWave
เพิ่มเจมใหม่ลงใน Gemfile:
Gemfile
gem 'carrierwave', '~> 1.0'
รัน:
bundle install
CarrierWave เก็บการตั้งค่าไว้ใน uploaders ที่จะถูก include เข้าไปในโมเดล สร้างอัปโหลดเดอร์ด้วยคำสั่ง:
rails generate uploader Image
ตอนนี้ใน app/uploaders จะมีไฟล์ image_uploader.rb ซึ่งมีคอมเมนต์และตัวอย่างที่เป็นประโยชน์ เดโมนี้ใช้ ActiveRecord แต่ CarrierWave ยังรองรับ Mongoid, Sequel, และ DataMapper ด้วย
ต่อไป ให้ include หรือ mount อัปโหลดเดอร์นี้เข้าไปในโมเดล:
models/post.rb
mount_uploader :image, ImageUploader
อัปโหลดเดอร์มีการตั้งค่าเริ่มต้นที่เหมาะสมแล้ว แต่ขั้นต่ำเราต้องเลือกที่เก็บไฟล์ที่อัปโหลด ใช้ file storage:
uploaders/image_uploader.rb
storage :file
โดยค่าเริ่มต้น ไฟล์จะถูกวางในโฟลเดอร์ public/uploads จึงควรเพิ่มเข้า .gitignore:
.gitignore
public/uploads
คุณยังสามารถแก้ไขเมธอด store_dir ภายในอัปโหลดเดอร์เพื่อเลือกที่ตั้งอื่นได้
ตอนนี้เราสามารถสร้างวิวใหม่และพาร์เชียลฟอร์มเพื่อเริ่มอัปโหลดไฟล์ได้:
views/posts/new.html.erb
<h1>Add post</h1> <%= render 'form', post: @post %>
views/posts/_form.html.erb
<%= form_for post do |f| %>
<div>
<%= f.label :title %>
<%= f.text_field :title %>
</div>
<div>
<%= f.label :body %>
<%= f.text_area :body %>
</div>
<div>
<%= f.label :image %>
<%= f.file_field :image %>
</div>
<%= f.submit %>
<% end %>
สังเกตว่า PostsController ไม่ต้องแก้ไข เพราะเราอนุญาตแอตทริบิวต์ image ไว้แล้ว
สุดท้าย สร้างวิว edit:
views/posts/edit.html.erb
<h1>Edit post</h1> <%= render 'form', post: @post %>
เสร็จแล้ว! ลองบูทเซิร์ฟเวอร์และสร้างโพสต์พร้อมรูปภาพได้เลย ปัญหาคือรูปภาพไม่แสดงที่ไหน มาดูส่วนต่อไปเพื่อเพิ่มหน้า show!
แสดงรูปภาพ
วิวที่ยังไม่สร้างคือ show มาเพิ่มกัน:
views/posts/show.html.erb
<%= link_to 'All posts', posts_path %> <h1><%= @post.title %></h1> <%= image_tag(@post.image.url, alt: 'Image') if @post.image? %> <p><%= @post.body %></p> <p><%= link_to 'Edit', edit_post_path(@post) %></p>
การแสดงไฟล์แนบง่ายมาก: ใช้ @post.image.url เพื่อดึง URL ของรูปภาพ สำหรับพาธของไฟล์ ใช้เมธอด current_path CarrierWave ยังมีเมธอด image? สำหรับตรวจสอบว่ามีไฟล์แนบหรือไม่ (เมธอด image จะไม่คืนค่า nil แม้ไม่มีไฟล์)
ตอนนี้ลองเข้าไปดูโพสต์ จะเห็นรูปภาพ แต่อาจดูใหญ่เกินไป เพราะเราไม่ได้จำกัดขนาด แม้จะใช้ CSS ย่อได้ แต่วิธีที่ดีกว่าคือสร้างภาพขนาดย่อหลังอัปโหลด ซึ่งต้องทำเพิ่มเติม
สร้างภาพขนาดย่อ (Thumbnails)
เพื่อครอปและปรับขนาดรูปภาพ ต้องใช้เครื่องมือแยกต่างหาก CarrierWave รองรับ RMagick และ MiniMagick ซึ่งใช้ ImageMagick ในการจัดการรูปภาพ ImageMagick เป็นโอเพนซอร์สสำหรับแก้ไขและสร้างรูปภาพ ต้องดาวน์โหลดและติดตั้งก่อน จะเลือกเจมไหนก็ได้ ผมเลือก MiniMagick เพราะติดตั้งง่ายและรองรับดีกว่า:
Gemfile
gem 'mini_magick'
รัน:
bundle install
include MiniMagick เข้าอัปโหลดเดอร์:
uploaders/image_uploader.rb
include CarrierWave::MiniMagick
เพิ่ม version ใหม่ชื่อ thumb:
uploaders/image_uploader.rb
version :thumb do process resize_to_fill: [350, 350] end
สามารถมี version ได้หลายตัว และ version สามารถสร้างจาก version อื่นได้:
uploaders/image_uploader.rb
version :small_thumb, from_version: :thumb do process resize_to_fill: [20, 20] end
หากอัปโหลดรูปไปแล้ว ไม่มี thumbnail ให้สร้างใหม่จาก Rails console:
rails c
Post.find_each {|post| post.image.recreate_versions!(:thumb) if post.image?}
แสดง thumbnail พร้อมลิงก์ไปรูปต้นฉบับ:
views/posts/show.html.erb
<%= link_to(image_tag(@post.image.thumb.url, alt: 'Image'), @post.image.url, target: '_blank') if @post.image? %>
บูทเซิร์ฟเวอร์และดูผลลัพธ์!
เพิ่มการตรวจสอบความถูกต้อง (Validations)
ตอนนี้อัปโหลดทำงานได้ แต่ยังไม่ตรวจสอบข้อมูลนำเข้า ซึ่งไม่ดี ถ้าต้องการเฉพาะรูปภาพ ให้ whitelist นามสกุล .png, .jpg, .gif:
uploaders/image_uploader.rb
def extension_whitelist %w(jpg jpeg gif png) end
ยังสามารถตรวจสอบ content type ด้วย content_type_whitelist:
uploaders/image_uploader.rb
def content_type_whitelist /image\// end
หรือ blacklist ไฟล์ที่ไม่ต้องการ เช่น executable ด้วย content_type_blacklist
นอกจากนี้ ให้จำกัดขนาดไฟล์ไม่เกิน 1 เมกะไบต์ ต้องใช้เจม file_validators:
Gemfile
gem 'file_validators'
ติดตั้ง:
bundle install
เพิ่ม validation ในโมเดล (รวม title และ body ด้วย):
models/post.rb
validates :title, presence: true, length: {minimum: 2}
validates :body, presence: true
validates :image, file_size: { less_than: 1.megabytes }
เพิ่มการแปลข้อความ error ของ CarrierWave:
config/locales/en.yml
en:
errors:
messages:
carrierwave_processing_error: "Cannot resize image."
carrierwave_integrity_error: "Not an image."
carrierwave_download_error: "Couldn't download image."
extension_whitelist_error: "You are not allowed to upload %{extension} files, allowed types: %{allowed_types}"
extension_blacklist_error: "You are not allowed to upload %{extension} files, prohibited types: %{prohibited_types}"
ยังไม่แสดง error ใดๆ มาสร้าง shared partial:
views/shared/_errors.html.erb
<% if object.errors.any? %>
<h3>Some errors were found:</h3>
<ul>
<% object.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
<% end %>
ใช้ partial นี้ในฟอร์ม:
views/posts/_form.html.erb
<%= render 'shared/errors', object: post %>
ลองอัปโหลดไฟล์ไม่ถูกต้อง จะเห็น error แต่ถ้าเลือกไฟล์ถูกต้องแต่ไม่กรอก title หรือ body จะ error และช่องไฟล์จะว่าง ต้องเลือกใหม่ ไม่สะดวก ต้องเพิ่มฟิลด์ hidden
เก็บไฟล์ไว้ข้ามคำขอ (Persisting Files Across Requests)
ง่ายมาก: เพิ่ม hidden field และ permit ในคอนโทรลเลอร์:
views/shared/_form.html.erb
<%= f.label :image %> <%= f.file_field :image %><br> <%= f.hidden_field :image_cache %>
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :image_cache)
ตอนนี้ image_cache จะถูกเติมอัตโนมัติ และรูปภาพไม่หายไป อาจแสดง thumbnail ให้ผู้ใช้เห็นว่าประมวลผลสำเร็จ:
views/shared/_form.html.erb
<% if post.image? %> <%= image_tag post.image.thumb.url %> <% end %>
ลบรูปภาพ
ฟีเจอร์ทั่วไปคือลบไฟล์แนบเมื่อแก้ไขเรคคอร์ด CarrierWave ทำได้ง่าย เพิ่ม checkbox ในฟอร์ม:
views/shared/_form.html.erb
<% if post.image? %>
<%= image_tag post.image.thumb.url %>
<div>
<%= label_tag :remove_image do %>
Remove image
<%= f.check_box :remove_image %>
<% end %>
</div>
<% end %>
Permit remove_image:
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache)
เสร็จแล้ว! ลบรูปด้วยตนเองใช้ remove_image!:
@post.remove_image!
อัปโหลดจากที่ตั้งระยะไกล (Remote Location)
CarrierWave มีฟีเจอร์อัปโหลดจาก URL มาให้ เพิ่มฟิลด์และ permit attribute:
views/shared/_form.html.erb
<%= f.text_field :remote_image_url %> <small>Enter URL to an image</small>
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url)
สบายมาก! ไม่ต้องแก้โค้ดเพิ่ม ทดสอบได้เลย
ทำงานกับการอัปโหลดหลายไฟล์ (Multiple Uploads)
ถ้าต้องการให้โพสต์มีหลายไฟล์แนบ ต้องเพิ่มฟิลด์ serialized (SQLite) หรือ JSON (Postgres/MySQL) ผมชอบ JSON เลยเปลี่ยน database adapter:
ลบ sqlite3 ออกจาก Gemfile เพิ่ม pg:
Gemfile
gem 'pg'
ติดตั้ง:
bundle install
แก้ config/database.yml:
config/database.yml
default: &default adapter: postgresql pool: 5 timeout: 5000 development: <<: *default database: upload_carrier_dev username: 'YOUR_USER' password: 'YOUR_PASSWORD' host: localhost
สร้าง database Postgres แล้วสร้างและรันไมเกรชัน:
rails g migration add_attachments_to_posts attachments:json rails db:migrate
ถ้าอยากใช้ SQLite ตามคู่มือ CarrierWave
Mount uploaders (สังเกต plural):
model/post.rb
mount_uploaders :attachments, ImageUploader
ใช้ uploader เดียวกัน แต่สร้างใหม่ก็ได้
เพิ่ม multiple file field ในฟอร์ม:
views/shared/_form.html.erb
<div> <%= f.label :attachments %> <%= f.file_field :attachments, multiple: true %> </div>
Permit attachments เป็น array:
posts_controller.rb
params.require(:post).permit(:title, :body, :image, :remove_image, :image_cache, :remote_image_url, attachments: [])
แสดง attachments โดยวนลูป:
views/shared/show.html.erb
<% if @post.attachments? %>
<ul>
<% @post.attachments.each do |attachment| %>
<li><%= link_to(image_tag(attachment.thumb.url, alt: 'Image'), attachment.url, target: '_blank') %></li>
<% end %>
</ul>
<% end %>
แต่ละ attachment จะมี thumbnail ตามที่ตั้งค่าใน ImageUploader เยี่ยม!
ใช้ที่เก็บข้อมูลบนคลาวด์ (Cloud Storage)
File storage ไม่สะดวกบน Heroku เพราะไม่เก็บไฟล์ได้ CarrierWave รองรับ Amazon S3 ผ่าน fog-aws:
Gemfile
gem "fog-aws"
ติดตั้ง:
bundle install
สร้าง initializer ตั้งค่า cloud storage ทั่วไป:
config/initializers/carrierwave.rb
CarrierWave.configure do |config|
config.fog_provider = 'fog/aws'
config.fog_credentials = {
provider: 'AWS',
aws_access_key_id: ENV['S3_KEY'],
aws_secret_access_key: ENV['S3_SECRET'],
region: ENV['S3_REGION'],
}
config.fog_directory = ENV['S3_BUCKET']
end
มีตัวเลือกอื่นๆ ในเอกสาร ผมใช้ dotenv-rails สำหรับ environment variables อย่าให้ S3 key pair เปิดเผยต่อสาธารณะ!
เปลี่ยน storage :file เป็น:
uploaders/image_uploader.rb
storage :fog
นอกจาก S3 ยังรองรับ Google Storage และ Rackspace ตั้งค่าได้ง่ายเช่นกัน
สรุป
จบแล้วครับ! เราครอบคลุมฟีเจอร์หลักของ CarrierWave แล้ว ตอนนี้คุณสามารถนำไปใช้ในโปรเจกต์ได้ ยังมีตัวเลือกเพิ่มเติม อ่านเอกสารต่อได้ หากคิดคิดไม่ออก ถามได้เลย CarrierWave wiki ก็มี how-to มากมาย ขอบคุณที่อ่าน Happy coding!