0

Elixir phoenix file upload

Đối với việc làm server thì việc upload các file dữ liệu hẳn là một việc không xa lạ gì với chúng ta. Hôm nay, mình sẽ cùng các bạn tìm hiểu về cách để upload một file lên server và để làm quen với nó mình sẽ làm ví dụ về việc upload avatar cho user. Việc này mình sẽ tiếp tục với project từ trước đó mà mình với các bạn đã làm (tại đây).

Tạo model

Mình sẽ làm ví dụ với trường avatar của bảng users. Do lần trước chúng ta đã tạo bảng user rồi nên bây giờ chỉ cần thêm trường avatar vào bảng user là xong. Với việc sử dụng để lưu file thì trường đó trong db (mysql) sẽ là string. Để thêm một trường vào database ta sử dụng lệnh sau để tạo file migrate:

mix ecto.gen.migration add_avatar_to_users

=> Ta sẽ có được một file migrate như sau:

# priv/repo/migrations/20170202064713_add_avatar_to_users.exs

defmodule HelloPhoenix.Repo.Migrations.AddAvatarToUsers do
  use Ecto.Migration

  def change do
  end
end

Khi đó ta muốn thêm hay xóa dữ liệu thì sẽ thêm các câu lệnh vào hàm change. Ví dụ ta thêm cột avatar kiểu string vào bang users thì sẽ viết như sau:

  def change do
    alter table(:users) do
      add :avatar, :string
    end
  end

Uploader với lib arc và arc_ecto

Cũng giống như ruby on rails. Ta cũng tạo một uploader chung để các trường có kiểu file update sẽ sử dụng chung config khi cần để ta sử dụng. Ở đây, ta sẽ tạo một uploader là avatar và ta cũng sử dụng thư viện arcarc_ecto tương tự như là carrierwave của rails. Thêm lib arcarc_ecto vào project:

# mix.exs
def application do
    ...
    applications: [..., :arc_ecto]
    ...
end

defp deps do
    [   ...
        {:arc_ecto, "0.3.2"},
        {:arc, "0.2.0"}
    ]
end

Đừng quên chạy mix deps.get để update 2 lib đó. Nếu bạn sử dụng S3 để lưu file thì sử dụng thêm các lib sau:

defp deps do
  [
    ...
    ex_aws: "~> 1.0.0",
    hackney: "1.6.1",
    poison: "~> 2.0",
    sweet_xml: "~> 0.5"
  ]
end

def application do
  [
    ...
    applications: [
        ...,
      :ex_aws,
      :hackney,
      :poison
    ]
  ]
end

Bây giờ ta tạo uploader avatar bằng lệnh:

mix arc.g avatar

Khi đó hệ thống sẽ tự tạo cho ta 1 file avatar default và ta phải thêm bớt nó theo ý của mình:

# web/uploaders/avatar.ex
defmodule HelloPhoenix.Avatar do
  use Arc.Definition
  use Arc.Ecto.Definition

  def __storage, do: Arc.Storage.Local

  # Include ecto support (requires package arc_ecto installed):

  @versions [:original]

  # To add a thumbnail version:
  # @versions [:original, :thumb]

  # Whitelist file extensions:
  # def validate({file, _}) do
  #   ~w(.jpg .jpeg .gif .png) |> Enum.member?(Path.extname(file.file_name))
  # end

  # Define a thumbnail transformation:
  # def transform(:thumb, _) do
  #   {:convert, "-strip -thumbnail 250x250^ -gravity center -extent 250x250 -format png"}
  # end

  # Override the persisted filenames:
  # def filename(version, _) do
  #   version
  # end

  # Override the storage directory:
  def storage_dir(version, {file, scope}) do
    "uploads/user/avatars"
  end

  # Provide a default URL if there hasn't been a file uploaded
  # def default_url(version, scope) do
  #   "/images/avatars/default_#{version}.png"
  # end
end

Với ví dụ trên thì mình thêm 2 dòng

  use Arc.Ecto.Definition

  def __storage, do: Arc.Storage.Local

mục đích là để lưu lại file dưới local. Nếu không các bạn có thể config với S3

config :arc,
  bucket: {:system, "S3_BUCKET"}
config :ex_aws,
  access_key_id: [{:system, "AWS_ACCESS_KEY_ID"}, :instance_role],
  secret_access_key: [{:system, "AWS_SECRET_ACCESS_KEY"}, :instance_role]

và sửa lại hàm

def storage_dir(version, {file, scope}) do
"uploads/users/avatars/#{scope.id}"
end

=>

 def storage_dir(version, {file, scope}) do
    "uploads/user/avatars"
 end

Do mình test nên để vậy cho dẽ theo dõi. Như vậy là đã tạo xong uploader và việc còn lại là mount trường dữ liệu tương ứng của model với uploader đó là xong.

#web/models/user.ex
    ...
    use Arc.Ecto.Model
    
    schema "users" do
        ...
        field :avatar, HelloPhoenix.Avatar.Type
        ...
     end
    

Vậy là xong với việc tạo uploader và mount chúng với dữ liệu tương ứng. Việc còn lại của ta chính là tạo view, controller để upload file lên và hiển thị chúng ra là xong.

View and controller

Việc này thì giống như việc tạo các controller và view thông thường khác. Chỉ khác biệt ở việc lấy đường dẫn của avatar. Không giống như config trên S3 ta định nghĩa host của url. Việc lưu file ở locale thì ta chỉ có thể lấy được path của url đó. Nên khi ta sử dụng thì cần chú ý đường dẫn. VD:

<img src="/<%= HelloPhoenix.Avatar.url({@user.avatar, @user}) %>", height="40" width="40">

Ta luôn phải thêm / vào đằng trước để link đúng là http://localhost:3000/uploads/xxx Khi không thêm thì nó sẽ là đường dẫn hiện tại + '/uploads/...' Như vậy sẽ không có được link đúng. Khi upload file lên thì ta phải config thêm path của thư mục chứa file đó để có thể truy nhập đến. Chú ý là ta thêm dòng config sau chứ ko phải là sửa lại dòng cũ nhé:

# lib/hello_phoenix/endpoint.ex
...
plug Plug.Static, at: "/uploads", from: Path.expand('./uploads'), gzip: false

Ta dùng avatar như file field bình thường

<div class="form-group">
  <label>Photo</label>
  <%= file_input f, :avatar, class: "form-control" %>
</div>

Như vậy là ta đã xong việc upload và hiển thị file (ảnh) ra khi sử dụng lib arc với arc_ecto. Ngoài ra các bạn có thể convert file, validate file, có các version :original, :thumb của 1 file gửi lên ... Việc đi sâu vào chi tiết các bạn có thể tham khảo tại https://github.com/stavro/arc

Source đầy đủ tại đây Demo

Tài liệu tham khảo

http://www.phoenixframework.org/docs/file-uploads https://github.com/stavro/arc https://smashingboxes.com/blog/image-upload-in-phoenix/


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.