8
votes

I have two product controllers in my elixir/phoenix backend. First - API endpoint (pipe_through :api) and second controller piping through :browser:

# router.ex
scope "/api", SecretApp.Api, as: :api do
  pipe_through :api

  resources "products", ProductController, only: [:create, :index]
end

scope "/", SecretApp do
  pipe_through :browser # Use the default browser stack

  resources "products", ProductController, only: [:new, :create, :index]
end

ProductController handles requests from form generated by elixir form helpers and accepts some file attachments. Everything is fine with it. Here is create action and params processed by this action:

def create(conn, %{"product" => product_params}) do
  changeset = Product.changeset(%Product{}, product_params)

  case Repo.insert(changeset) do
    {:ok, _product} ->
      conn
      |> put_flash(:info, "Product created successfully.")
      |> redirect(to: product_path(conn, :index))
    {:error, changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

params from log (I am using arc for handling image uploads in elixir code)

[debug] Processing by SecretApp.ProductController.create/2
  Parameters: %{"_csrf_token" => "Zl81JgdhIQ8GG2c+ei0WCQ9hTjI+AAAA0fwto+HMdQ7S7OCsLQ9Trg==", "_utf8" => "✓", 
              "product" => %{"description" => "description_name", 
                "image" => %Plug.Upload{content_type: "image/png", 
                  filename: "wallpaper-466648.png", 
                  path: "/tmp/plug-1460/multipart-754282-298907-1"}, 
                "name" => "product_name", "price" => "100"}}
  Pipelines: [:browser]

Api.ProductController handles requests from redux-from. Here is action, view and params, which are processed by this action:

# action in controller
def create(conn, %{"product" => product_params}) do
  changeset = Product.changeset(%Product{}, product_params)

  case Repo.insert(changeset) do
    {:ok, _product} ->
      conn
      |> render("index.json", status: :ok)
    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render("error.json", changeset: changeset)
  end
end

# product_view.ex
def render("index.json", resp=%{status: status}) do
  %{status: status}
end

def render("error.json", %{changeset: changeset}) do
  errors = Enum.into(changeset.errors, %{})

  %{
    errors: errors
  }
end

[info] POST /api/products/
[debug] Processing by SecretApp.Api.ProductController.create/2
  Parameters: %{"product" => %{"description" => "product_description", "image" => "wallpaper-466648.png", "name" => "product_name", "price" => "100"}}
  Pipelines: [:api]
[info] Sent 422 in 167ms

Create action fails with 422 status, because image can't be saved with these params. My problem that I can't access image from backend code, I only have it in my JS code as FileList object. I don't understand how to pass image to backend code. Here is how this attachment represented in my JS code (FileList, containing information about uploaded image).

value:FileList
  0: File
    lastModified: 1381593256801
    lastModifiedDate: Sat Oct 12 2013 18:54:16 GMT+0300 
    name: "wallpaper-466648.png"
    size: 1787293
    type: "image/png"
    webkitRelativePath: ""

I only have WebkitRelativePath (In case with first controller I have path to image: "/tmp/plug-1460/multipart-754282-298907-1") and I don't know what can I do with this JS object and how to access real image represented by this JS object (here is a redux-form reference about file uploads).

Could you help me? How to explain to elixir how to find an image? I just would like to submit file attachments to my backend using JS code (because there a lot of interesting features for async validation etc).

Here is a link to a full app if it could be helpful

2
Static assets like pictures should live under priv/static/. Can you find the file under priv/static/tmp/plug-1460/multipart-754282-298907-1? If not, try storing the image there.tkowal
My problem that I can't access image from backend code, I only have it in my JS code as FileList object. I don't understand how to pass image to backend code. There is nothing in priv/static/tmp howeverMihail Davydenkov

2 Answers

3
votes

Finally I've managed to solve this problem. The solution is in correct serialization of redux-form submitted params.

Here is my redux form, starting point of the request:

// product_form.js

import React, { PropTypes } from 'react';
import {reduxForm} from 'redux-form';

class ProductForm extends React.Component {
  static propTypes = {
    fields: PropTypes.object.isRequired,
    handleSubmit: PropTypes.func.isRequired,
    error: PropTypes.string,
    resetForm: PropTypes.func.isRequired,
    submitting: PropTypes.bool.isRequired
  };

  render() {
    const {fields: {name, description, price, image}, handleSubmit, resetForm, submitting, error} = this.props;

    return (
      <div className="product_form">
        <div className="inner">
          <form onSubmit={handleSubmit} encType="multipart/form-data">
            <div className="form-group">
              <label className="control-label"> Name </label>
              <input type="text" className="form-control" {...name} />
              {name.touched && name.error && <div className="col-xs-3 help-block">{name.error}</div>}
            </div>

            <div className="form-group">
              <label className="control-label"> Description </label>
              <input type="textarea" className="form-control" {...description} />
              {description.touched && description.error && <div className="col-xs-3 help-block">{description.error}</div>}
            </div>

            <div className="form-group">
              <label className="control-label"> Price </label>
              <input type="number" step="any" className="form-control" {...price} />
              {price.touched && price.error && <div className="col-xs-3 help-block">{price.error}</div>}
            </div>

            <div className="form-group">
              <label className="control-label"> Image </label>
              <input type="file" className="form-control" {...image} value={ null } />
              {image.touched && image.error && <div className="col-xs-3 help-block">{image.error}</div>}
            </div>

            <div className="form-group">
              <button type="submit" className="btn btn-primary" >Submit</button>
            </div>
          </form>
        </div>
      </div>
    );
  }
}

ProductForm = reduxForm({
  form: 'new_product_form',
  fields: ['name', 'description', 'price', 'image']
})(ProductForm);

export default ProductForm;

This form passes the following params to the function handleSubmit after user presses the button "Submit"

# values variable
Object {name: "1", description: "2", price: "3", image: FileList}

# where image value is 
value:FileList
  0: File
    lastModified: 1381593256801
    lastModifiedDate: Sat Oct 12 2013 18:54:16 GMT+0300 
    name: "wallpaper-466648.png"
    size: 1787293
    type: "image/png"
    webkitRelativePath: ""

To pass these params to backend I am using the FormData Web API and the file-upload request using isomorphic-fetch npm module

Here is the code did the trick:

// product_form_container.js (where form submit processed, see _handleSubmit function)

import React                   from 'react';
import ProductForm             from '../components/product_form';
import { Link }                from 'react-router';
import { connect }             from 'react-redux';
import Actions                 from '../actions/products';
import * as form_actions            from 'redux-form';
import {httpGet, httpPost, httpPostForm} from '../utils';

class ProductFormContainer extends React.Component {
  _handleSubmit(values) {
    return new Promise((resolve, reject) => {
      let form_data = new FormData();

      Object.keys(values).forEach((key) => {
        if (values[key] instanceof FileList) {
          form_data.append(`product[${key}]`, values[key][0], values[key][0].name);
        } else {
          form_data.append(`product[${key}]`, values[key]);
        }
      });

      httpPostForm(`/api/products/`, form_data)
      .then((response) => {
        resolve();
      })
      .catch((error) => {
        error.response.json()
        .then((json) => {
          let responce = {};
          Object.keys(json.errors).map((key) => {
            Object.assign(responce, {[key] : json.errors[key]});
          });

          if (json.errors) {
            reject({...responce, _error: 'Login failed!'});
          } else {
            reject({_error: 'Something went wrong!'});
          };
        });
      });
    });
  }

  render() {
    const { products } = this.props;

    return (
      <div>
        <h2> New product </h2>
        <ProductForm title="Add product" onSubmit={::this._handleSubmit} />

        <Link to='/admin/products'> Back </Link>
      </div>
    );
  }
}

export default connect()(ProductFormContainer);

where httpPostForm is a wrapper around fetch:

export function httpPostForm(url, data) {
  return fetch(url, {
    method: 'post',
    headers: {
      'Accept': 'application/json'
    },
    body: data,
  })
  .then(checkStatus)
  .then(parseJSON);
}

And that's it. There was nothing to fix in my elixir code, Api.ProductController remains the same (see initial post). But now it receives request with the following params:

[info] POST /api/products/
[debug] Processing by SecretApp.Api.ProductController.create/2
  Parameters: %{"product" => %{
                "description" => "2", 
                "image" => %Plug.Upload{
                  content_type: "image/jpeg",
                  filename: "monkey_in_jungle-t3.jpg", 
                  path: "/tmp/plug-1461/multipart-853391-603088-1"
                }, 
               "name" => "1", 
               "price" => "3"}}
  Pipelines: [:api]

Many thanks for everyone trying to help me. Hope this could help someone struggling with similar serialization issues.

2
votes

From your log, it's clear that image is making it from the browser to the controller.

The File Uploads guide in the Phoenix docs should be helpful to you: http://www.phoenixframework.org/docs/file-uploads

From the docs:

Once we have the Plug.Upload struct available in our controller, we can perform any operation on it we want. We can check to make sure the file exists with File.exists?/1, copy it somewhere else on the filesystem with File.cp/2, send it to S3 with an external library, or even send it back to the client with Plug.Conn.send_file/5.

I think what is happening in your case is that the uploaded file is deleted when the process ends since you didn't save the temporary version of it to somewhere else. (I am assuming you aren't already stashing it in the DB.) I would write the code to do this into your controller(s) after you verify the changeset is valid.