almost 5 years ago

這次作業的目標是讓 Rails App 可以上傳圖片到 AWS S3 ,本篇重點記錄設定和程式碼

先改變 model Video 的結構,這是因為之後用 Carrierwave 這個 gem ,它會提供我們輔助方法,所以就不再需要 small_cover_url, large_cover_url 這兩個欄位

  add_column :videos, :small_cover, :string
  add_column :videos, :large_cover, :string  
  remove_column :videos, :small_cover_url
  remove_column :videos, :large_cover_url


相關 gem 的安裝

首先,在 Rails 處理檔案上傳的解法之一是用 Carrierwave 這個 gem ,可以參考 Railscasts 的教學,此外,它也能跟 S3 良好的協調運作,是本情境中很好的解決方案,要用 Carrierwave 上傳檔案到 AWS S3 還需要 fog, fog-aws 這兩個 gem ,如果想要圖片可以有不同大小版本,可以用 MiniMagick 這個 gem ,所以新增下面內容到 Gemfile ,然後 bundle install

Gemfile
gem 'carrierwave'
gem "mini_magick"
gem "fog"
gem 'fog-aws'


Carrierwave 相關設定

Carrierwave 提供 generator 的指令,讓你可以很方便產生處理上傳需要的程式碼,還內附註解說明常用功能,以我的情況來說,我要處理的東西命名為 small_cover, large_cover ,是 Video 這個 model 的兩個屬性,所以我就輸入以下指令:

$ rails generate uploader small_cover
$ rails generate uploader large_cover

接下來我必須在對應的 model Video 裡加入設定:

app/models/video.rb
 class Video < ActiveRecord::Base
   mount_uploader :large_cover, LargeCoverUploader
   mount_uploader :small_cover, SmallCoverUploader
 end
end

設定 SmallCoverUploaderLargeCoverUploader

small_cover.rb
# encoding: utf-8


class SmallCoverUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick  # 如此才能呼叫 MiniMagick 提供的方法


  if Rails.env.test?
    storage :file  # 設定儲存方式為本機檔案

  else
    storage :fog  # 設定儲存方式為 AWS S3 ,另外要用一個 gem 'fog'

  end

  def store_dir
    "uploads"  # 上傳檔案放到 uploads 這個資料夾

  end

  def extension_white_list
    %w(jpg jpeg gif png)  # 只允許上傳這些類型的檔案

  end

  process resize_to_fill: [166, 236]  # 網頁要取得圖片來顯示之前,先把圖片調整成這個大小,不影響原圖片

end
large_cover.rb
# encoding: utf-8


class LargeCoverUploader < CarrierWave::Uploader::Base
  include CarrierWave::MiniMagick  # 如此才能呼叫 MiniMagick 提供的方法


  if Rails.env.test?
    storage :file  # 設定儲存方式為本機檔案

  else
    storage :fog  # 設定儲存方式為 AWS S3 ,另外要用一個 gem 'fog'

  end

  def store_dir
    "uploads"  # 上傳檔案放到 uploads 這個資料夾

  end

  def extension_white_list
    %w(jpg jpeg gif png)  # 只允許上傳這些類型的檔案

  end

  process resize_to_fill: [665, 375]  # 網頁要取得圖片來顯示之前,先把圖片調整成這個大小,不影響原圖片

end

接下來還要做 Carrierwave 的設定,我用 figaro 這個 gem 管理一些隱秘資料(API keys),把它們的值寫在 config/application.yml 這個檔案,這些資料就會存在作業系統的環境變數裡,再在 Rails app 裡存取它們,比較安全

以下這麼設定的原因是, test 環境不需要 AWS 相關設定,直接存在本機 Rails 專案的資料夾處理就好,而其他環境 production, staging, development 都要有存取 AWS 需要的 ID, KEY, REGION 等資訊,另外, config.enable_processing = false 是 Carrierwave 說明裡推薦的,可以加速測試進行

config/initializers/carrierwave.rb
CarrierWave.configure do |config|
  if Rails.env.test?
    config.storage = :file
    config.enable_processing = false
    
  else
    config.storage = :fog
    config.fog_credentials = {
      provider:              'AWS',
      aws_access_key_id:     ENV['AWS_ACCESS_KEY_ID'],
      aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
      region:                ENV['AWS_REGION']
    }

    if Rails.env.production?
      config.fog_directory  = ENV['AWS_S3_BUCKET_PRODUCTION']
    elsif Rails.env.staging?
      config.fog_directory  = ENV['AWS_S3_BUCKET_STAGING']
    elsif Rails.env.development?
      config.fog_directory  = ENV['AWS_S3_BUCKET_DEVELOPMENT']
    end
  end
end


對應的 view 的修改

上傳欄位的部分改成這樣:

app/views/admin/videos/new.html.haml
= f.file_field :large_cover, label: "Large Cover"
= f.file_field :small_cover, label: "Small Cover"  

之前存取圖片是用 Video#small_cover_url, Video#large_cover_url ,現在改用 Video#small_cover, Video#large_cover

app/views/videos/index.html.haml
   - category.recent_videos.each do |video|
     .video.col-sm-2
       = link_to video, nil do
         %img(src="#{video.small_cover}")


設置存取 AWS 需要的環境變數

其實我在這部分卡滿久,首先閱讀這份說明,教你怎麼在 AWS 上建立「子使用者」, AWS 的服務很多, root (你註冊的 AWS 帳號)對所有服務都有存取和使用權限,相對的,被駭了可能就很危險,所以在其底下建立「子使用者」(這是我編的詞方便稱呼而已),設定這個子使用者只能存取 S3 ,其他都不行,這樣這個子使用者被駭,存取的範圍也就只限於你的 S3 而已

照著上一段的那份說明,建立好「子使用者」之後應該就能得到 AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY ,接下來還需要在 S3 建立 buckets , 'AWS_S3_BUCKET_PRODUCTION', 'AWS_S3_BUCKET_STAGING', 'AWS_S3_BUCKET_DEVELOPMENT' 這些對應的是你的 bucket name ,最後一個 Region ,填的可不是建立 bucket 時選的地區名!參考這份文件,如果當初選 Oregon ,那 Region 對應的值就要是 us-west-2


修改 seed data 的程式碼以正常 seed

這也是我花很多時間的一部份,原本的 seeds.rb :

seeds.rb
Video.create(title: "Attack On Titan", description: "Top 1 anime in 2013", small_cover_url: "/tmp/attack_on_titan.jpg", large_cover_url: "/tmp/attack_on_titan_large.jpg", category: anime)
Video.create(title: "Grande Road", description: "A passionate anime", small_cover_url: "/tmp/grande_road.jpg", category: anime)
Video.create(title: "Tokyo Ghoul", description: "Amazing...", small_cover_url: "/tmp/tokyo_ghoul.jpg", category: anime)
Video.create(title: "Kiseji", description: "Owesome!", small_cover_url: "/tmp/kiseji.jpg", category: anime)
Video.create(title: "Fate Zero", description: "Good", small_cover_url: "/tmp/fate_zero.jpg", large_cover_url: "/tmp/fate_zero_large.jpg", category: anime)
Video.create(title: "Hunter X Hunter", description: "Five stars", small_cover_url: "/tmp/hunter_hunter.jpg", category: anime)
Video.create(title: "Fullmetal Alchemist", description: "Not bad", small_cover_url: "/tmp/fullmetal_alchemist.jpg", large_cover_url: "/tmp/fullmetal_alchemist_large.jpg", category: anime)
Video.create(title: "Psycho Pass", description: "Amazing", small_cover_url: "/tmp/psycho_pass.jpg", large_cover_url: "/tmp/psycho_pass_large.jpg", category: anime)

後來我根據我前一篇文章「練習用 Ruby 對 CSV 檔案簡易操作」的內容改成這樣:

db/seeds.rb
require 'csv'

anime = Category.create(name: "Anime")
Category.create(name: "Movie")

videos_data = CSV.read "db/videos_information.csv"
headers = videos_data.shift.map {|header| header.to_sym}
array_of_hashes = videos_data.map {|video_data| Hash[*headers.zip(video_data).flatten] }

directory = "#{Rails.root}/public/tmp/"

array_of_hashes.each do |video_params|
  video_params[:small_cover] = File.open(directory + video_params[:small_cover])
  video_params[:large_cover] = File.open(directory + video_params[:large_cover]) if video_params[:large_cover]
  video_params[:category] = Category.find_by(name: video_params[:category])

  Video.create(video_params)
end


檔案上傳功能的測試

我查到這個討論串提到可以用 fixture_file_upload 這個方法,因為我是用 rspec ,所以把要用於測試中上傳的檔案放在 spec/fixtures 這個資料夾裡,使用時再把檔名當成參數丟進去就行了

spec/controllers/admin/videos_controller_spec.rb
  describe "POST create" do
    it_behaves_like "require_sign_in" do
      let(:action) { post :create }
    end

    it_behaves_like "require admin" do
      let(:action) { post :create }
    end

    context "when the current user is an administrator" do
      before { set_admin_current_user }

      context "with valid input" do
        after do
          delete_files_uploaded_by_tests
        end

        it "creates a video with small cover and large_cover" do
          post :create, video: Fabricate.attributes_for(:video, small_cover: fixture_file_upload('fate_stay_night.jpg'), large_cover: fixture_file_upload('fate_stay_night_large.png'))
          expect(Video.count).to eq(1)
          expect(Video.first.small_cover.file.path).to_not be_nil
          expect(Video.first.large_cover.file.path).to_not be_nil
        end

        it "redirects to the home page" do
          post :create, video: Fabricate.attributes_for(:video, small_cover: fixture_file_upload('fate_stay_night.jpg'), large_cover: fixture_file_upload('fate_stay_night_large.png'))
          expect(response).to redirect_to home_path
        end

        it "sets a success message" do
          post :create, video: Fabricate.attributes_for(:video, small_cover: fixture_file_upload('fate_stay_night.jpg'), large_cover: fixture_file_upload('fate_stay_night_large.png'))
          expect(flash[:success]).to_not be_nil
        end
      end

      context "with invalid input" do
        it "does not create a video" do
          post :create, video: Fabricate.attributes_for(:video, title: "")
          expect(Video.count).to eq(0)
        end

        it "sets @video" do
          post :create, video: Fabricate.attributes_for(:video, title: "")
          expect(assigns(:video)).to be_a(Video)
        end

        it "renders the new template" do
          post :create, video: Fabricate.attributes_for(:video, title: "")
          expect(response).to render_template :new
        end
      end
    end
  end

此外,因為測試還是會真的上傳檔案,只是是傳到本機伺服器處理,所以 Rails 專案會多出一些圖檔,以我的設定來說,因為我在 SmallCoverUploader, LargeCoverUploader 裡設定上傳到 uploads 這個資料夾,我在第一次跑完檔案上傳功能的測試之後就發現多了 /public/uploads 這個資料夾,裡面還有幾張圖檔,我想在每次測試跑完後自動把它們刪除,所以我寫了一個 after block ,呼叫一個自定義的方法 delete_files_uploaded_by_tests ,它會幫我把因為上傳而多出來的資料夾和檔案都刪除

spec/support/macros.rb
def delete_files_uploaded_by_tests
  FileUtils.rm_rf(Dir["#{Rails.root}/public/uploads"])
end

以上就是這周課程關於檔案上傳的部分,其實在這之前還有設定 users table 一個新欄位 admin ,和如何處理網站中不同角色的問題,之後補上

← 練習用 Ruby 對 CSV 檔案簡易操作 簡易的設定 administrator 和處理 →
 
comments powered by Disqus