Вложения файлов для redux-form и elixir/phoenix как backend API (проблема сериализации)
у меня есть два product
контроллеры в моем бэкэнде elixir / phoenix. Первая конечная точка API (pipe_through :api
) и второй контроллер 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
обрабатывает запросы из формы, сгенерированной помощниками формы elixir, и принимает некоторые вложения файлов. Все в порядке. Вот создать действие и параметры, обработанные этим действием:
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 из журнала (я использую arc для обработки загрузки изображений в elixir код)
[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
обрабатывает запросы от redux-from. Вот действие, вид и параметры, которые обрабатываются этим действием:
# 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
создать действие не удается с 422 статусом, потому что изображение не может быть сохранено с помощью этих параметров. Моя проблема в том, что я не могу получить доступ к изображению из бэкэнд-кода, у меня есть только в моем JS-коде как объект FileList. Я не понимаю, как передать изображение с backend кода. Вот как это вложение представлено в моем JS-коде (FileList, содержащий информацию о загруженном изображении).
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: ""
у меня только WebkitRelativePath (в случае с первым контроллером у меня путь к изображению: "/tmp/и вилка-1460/составные-754282-298907-1") и я не знаю, что мне делать с это JS объект и как открыть реальный образ представлял это JS объекта (вот это redux-ссылка на форму о загрузка файлов).
не могли бы вы помочь мне? Как объяснить эликсиру, как найти изображение? Я просто хочу ... чтобы отправить вложения файлов на мой бэкэнд, используя JS-код (потому что есть много интересных функций для асинхронной проверки и т. д.).
вот ссылка на полный app если это может быть полезным
2 ответов
наконец-то мне удалось решить эту проблему. Решение находится в правильной сериализации redux-form представленных параметров.
вот моя форма redux, отправная точка запроса:
// 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;
эта форма передает следующие параметры функции handleSubmit
после нажатия пользователем кнопки "Отправить"
# 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: ""
чтобы передать эти параметры в бэкэнд, я использую FormData Web API и file-запрос на загрузку с помощью isomorphic-fetch npm module
вот код сделал трюк:
// 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);
здесь httpPostForm
обертка вокруг fetch:
export function httpPostForm(url, data) {
return fetch(url, {
method: 'post',
headers: {
'Accept': 'application/json'
},
body: data,
})
.then(checkStatus)
.then(parseJSON);
}
и это все. В моем коде эликсира не было ничего, что можно было бы исправить,Api.ProductController
остается прежним (см. начальный пост). Но теперь он получает запрос со следующими параметрами:
[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]
большое спасибо всем, кто пытается мне помочь. Надеюсь, это может помочь кому-то бороться с подобным проблемы сериализации.
из вашего журнала ясно, что изображение делает его из браузера на контроллер.
руководство по загрузке файлов в документах Phoenix должно быть полезно для вас: http://www.phoenixframework.org/docs/file-uploads
документы:
Как только у нас будет вилка.Структура загрузки доступная в нашем регуляторе, мы можем выполнить любую деятельность на ей мы хотим. Мы можем проверить, чтобы убедиться, что файл существует с файлом.существует?/1, скопируйте его куда-нибудь остальное в файловой системе с файлом.cp / 2, отправьте его в S3 с внешней библиотекой или даже отправьте его обратно клиенту с помощью Plug.Коннектикут.send_file/5.
Я думаю, что в вашем случае происходит то, что загруженный файл удаляется, когда процесс заканчивается, так как вы не сохранили временную версию его в другом месте. (Я предполагаю, что вы еще не прячете его в DB.) Я бы написал код для этого в ваш контроллер(Ы) после проверки набора изменений действительный.