elixir-waffle / waffle

Flexible file upload and attachment library for Elixir
727 stars 71 forks source link

LiveView support #71

Open mayel opened 3 years ago

mayel commented 3 years ago

With the new upload functionality bundled in Phoenix LiveView, I wonder if anyone has successfully used Waffle with it, and if there are any gotchas or changes needed in the lib?

Info about LiveView uploads: https://hexdocs.pm/phoenix_live_view/uploads.html#content and https://hexdocs.pm/phoenix_live_view/uploads-external.html

achedeuzot commented 3 years ago

I've currently used Waffle (without Waffle Ecto) with Phoenix LiveView.

I followed the phoenix blog & screencast https://www.phoenixframework.org/blog/phoenix-live-view-upload-deep-dive with a few changes:

The rest was quite straightforward to implement :)

achempion commented 3 years ago

@achedeuzot Could you please describe what issues did you have with waffle_ecto library? It provides couple of convenient functions like cast_attachements for the changeset as well as cache management and url generation which you still need for the LiveView.

achedeuzot commented 3 years ago

@achempion Indeed, cast_attachements seemed perfect for what I wanted but I couldn't see how to make it work with the consume_uploaded_entries function from LiveView if I followed the sample video from the Phoenix blog. I moved the consume_uploaded_entries before the call to the changeset to try and give the file path into cast_attachements but I couldn't make it work.

If I look at the code of waffle_ecto, I can see cast_attachements accepts a %Plug.Upload{} which I don't have (LiveViews provides a %LiveView.UploadEntry{}), a %{filename: filename, binary: binary} map which I don't have either (and I don't want to read the file into the :binary key for memory consumption reasons) or a simple file path which looks like what I want but fails with :invalid_path even if I set accept_paths: true, the file path given by consume_uploaded_entries gives something like /var/folders/e4/rnd23gsflk23_ynglbkeshdq9_m02340gq/T//plug-1704/live_view_upload-094958382-29457213941-4.

Right now my steps are the following: 1 - validate schema params through a standard changeset function 2 - validate uploaded files through the MyApp.AvatarUploader.validate/1 function and add errors into the changeset if needed 3 - save the record to database if everything is OK 4 - save the file to storage if everything is OK. If not, I'm rolling back the field in the DB.

I also couldn't see where I could call a custom validate function on the passed uploaded file using cast_attachements.

Finally, the way the MyApp.AvatarUploader module needs to contain use Waffle.Ecto.Definition and the schema must contain field: :avatar, MyApp.AvatarUploader.Type created a dependency schema which doesn't work well with my current application setup: the MyApp.AvatarUploader lives in an app that depends on the Ecto schema app but not the other way around. So it's creating a circular dependency between apps I'd rather not create.

pierre-pretorius commented 3 years ago

My app is completely written in LiveView. Doesn't seem like Waffle needs special support for LiveView. I followed this guide: https://www.phoenixframework.org/blog/phoenix-live-view-upload-deep-dive

And implemented the consume method like this. Note I have max_entries set to 1 so I'm only expecting one file.

def consume_logo(socket, %Organization{} = organization) do
    consume_uploaded_entries(socket, :logo, fn meta, _entry -> {:ok, _} = Uploaders.Logo.store({meta.path, organization}) end)
    {:ok, organization}
end
montebrown commented 3 years ago

Hi @achedeuzot,

You can use waffle_ecto - I was able to get this working without changing my schema. You can construct a %Plug.Upload{} and pass it in to the changeset. Something like:

def handle_event("save", _params, socket) do
  consume_uploaded_entries(socket, :avatar, fn meta, entry ->
    {:ok, user} =
      Accounts.update_user(socket.assigns.current_user, %{
        "avatar" => %Plug.Upload{
          content_type: entry.client_type,
          filename: entry.client_name,
          path: meta.path
        },
      })

    Snap.Uploaders.Avatar.url({user.avatar, user}, :original)
  end)

  {:noreply, socket |> update(:uploaded_files, &(&1 ++ uploaded_files)) |> fetch_user()}
end
achedeuzot commented 3 years ago

Hi @montebrown !

Thanks for your solution 🤗

My main issue was to find a way to properly manage the user update transaction as well as the possible errors of the validation and your solution is spot on, thanks ! I'll just need to raise in case of an {:error, %Ecto.Changeset{}} return value to the Accounts.update_user so I can show proper error messages.

My last issue to resolve now with waffle is how to use multiple S3 buckets that each have their own access tokens :P

TomEversdijk commented 2 years ago

Thanks @montebrown for your solution!

For everybody who tried to get this working it is important to use the cast_attachements method within the consume_uploaded_entries. The consume_uploaded_entries will automatically delete the file-path when it is finished so otherwise and {:error, :invalid_file_path} error will be returned.

stratigos commented 3 months ago

Thanks to everyone in the history above that helped me find my way with handling the file uploads with today's LiveView of 0.20.15.

Given this:

For everybody who tried to get this working it is important to use the cast_attachements method within the consume_uploaded_entries. The consume_uploaded_entries will automatically delete the file-path when it is finished so otherwise and {:error, :invalid_file_path} error will be returned.

The consume_uploaded_entries function's closure now can have a return type of {:postpone, result}, which allows the tmp file to persist and be handled after consume_uploaded_entries finishes (which allows waffle to handle the changeset as normal).

https://hexdocs.pm/phoenix_live_view/0.20.14/Phoenix.LiveView.html#consume_uploaded_entries/3

I found success with the following:

# I have `max_entries: 1` set, the following expects only one `entry`:
def handle_event(
  "save",
  %{"schema" => schema_params},
  socket
) do
  [schema_params_with_image] =
    consume_uploaded_entries(socket, :image, fn meta, entry ->
      image_plug_upload =
        %Plug.Upload{
          content_type: entry.client_type,
          filename: entry.client_name,
          path: meta.path
        }

      updated_params =
        Map.put(schema_params, "image", image_plug_upload)

      {:postpone, updated_params}
    end)

  save_schema(
    socket,
    socket.assigns.action,
    schema_params_with_image
  )
end