2
votes

I created a simple "Thing" ecto model in Phoenix. The "image" is a string that references a file that gets uploaded. When an uploaded image hits the controller, I'd like to transform the image struct into a string (for the sake of this example, let's save it as "foo123")

#/web/models/thing.ex

defmodule MyApp.Thing do
  use MyApp.Web, :model

  schema "comics" do
    field :number, :integer
    field :image, :string

    timestamps()
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:number, :image])
    |> validate_required([:number, :image])
  end
end

#/web/controllers/thing_controller.ex

  def create(conn, %{"thing" => thing_params}) do
    IO.inspect thing_params

    changeset = Thing.changeset(%Thing{}, thing_params) 
       |> change("image", "foo123") ### something like this

    case Repo.insert!(changeset) do
      {:ok, _thing} ->
        conn
        |> put_flash(:info, "Thing created successfully.")
        |> redirect(to: thing_path(conn, :index))
      {:error, changeset} ->
        conn
        |> put_flash(:error, "Something went wrong")
        |> redirect(to: thing_path(conn, :index))
    end
  end

This doesn't work, I still need to transform %{"image" => %Plug.Upload{content_type: "image/jpeg",... into "foo123", but where does that happen? In the changeset assignment? In the parameters for def create()?

Also, how can I IO.inspect just a certain member of the param struct? (Like, IO.inspect thing_params.image?

1
For the last question, you can do IO.inspect thing_params["image"]. .image won't work because the key is a string, not an atom.Dogbert
For the main question, you can use |> put_change(:image, "foo123") but that won't re-run the validations of Thing.changeset/2. I'm not sure what the best way to handle file uploads manually is so I'm just commenting here instead of posting an answer.Dogbert
@Dogbert This is actually the answer I am looking for. Where does put_change() go? When I run changeset = Thing.changeset(%Thing{}, thing_params) |> put_change(:image, "foo123"), I get ** (CompileError) web/controllers/thing_controller.ex:19: undefined function put_change/3. Is this the wrong place for it, or did I forget to import something into the controller?Mark Karavan
@Dogbert: This was exactly what I was looking for, thanks. If you want to post this, I'll mark it as correct. ` changeset = Thing.changeset(%Comic{}, comic_params) |> Ecto.Changeset.put_change(:image, "foo123")`Mark Karavan

1 Answers

1
votes

When the multipart get to your controller, the Plug.Upload has already handled the download and placed it into a temporary file. That filename can be found in your %Plug.Upload{} struct. It is up to you to copy that file to a permeant location, choosing what ever name you want to use. Then you can put that name into the image field or your schema. You must do that in your controller since the file get removed once the request ends.

I suggests you look at ArchEcto. It does a great job handling file attachments and provides nice APIs for getting urls put put in your views.

You can also insert image processing to create thubnails and the such. Here is a bit of my code that handles image and video uploads. For the images, I save a full res and a poster sized view (for display in a chat app). For the videos, I create a poster image which I use to display the video in the chat app.

defmodule UcxChat.File do
  use Arc.Definition

  # Include ecto support (requires package arc_ecto installed):
  use Arc.Ecto.Definition
  require Logger

  def __storage, do: Arc.Storage.Local

  @versions [:original, :poster]

  @acl :public_read

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

  # Whitelist file extensions:
  def validate({file, _}) do
    ~w(.jpg .jpeg .gif .png .txt .text .doc .pdf .wav .mp3 .mp4 .mov .m4a .xls) |> Enum.member?(Path.extname(file.file_name))
  end

  def transform(:poster, {_, %{type: "video" <> _}}) do
    {:ffmpeg, fn(input, output) ->
      "-i #{input} -f image2 -ss 00:00:01.00 -vframes 1 -vf scale=-1:200 #{output}" end, :jpg}
  end
  def transform(:poster, {_, %{type: "image" <> _}}) do
    {:convert, "-strip -resize @80000 -format png", :png}
  end

  def filename(:poster, _params) do
    :poster
  end
  def filename(_version, {_, %{file_name: file_name}}) do
    String.replace(file_name, ~r(\.[^/.]+$), "")
  end
  def filename(_version, %{file_name: file_name}) do
    file_name
  end

  def storage_dir(_version, {_file, scope}) do
    storage_dir(scope)
  end
  def storage_dir(scope) do
    path = "priv/static/uploads/#{scope.message_id}"
    if UcxChat.env == :prod do
      Path.join(Application.app_dir(:ucx_chat), path)
    else
      path
    end
  end

end

Even if you don't want to use the package, it will probably give you some ideas on how to role your own.