So far everything works in the backend regarding Rails and Paperclip. I have it set up so that when you create a new Post, the image within that post will be uploaded to AWS S3. It works fine on the backend. I am also using Expo's ImagePicker to grab an image from the users camera roll. However, when I'm trying to post something with Axios in React Native using formData in the front end, I'm getting this error:
Paperclip::AdapterRegistry::NoHandlerError - No handler found for
"file:///Users/jimmy/Library/Developer/CoreSimulator/Devices/A3590E6A-
281B-4EFB-868C-9F5311718832/data/Containers/Data/Application/CB5ED945-
AA87-4E36-83B6-71426759C70C/Library/Caches/ExponentExperienceData/%2540anonymous%252Fartis
-ed15f4f4-b8d6-460f-a1b3-e06d546eda2a/ImagePicker/B3162237-9BCD-4721-
BDF2-D66BC047A6C0.jpg"
Here's some more snippets of my code:
React Native Submit handler in the PostForm:
onSubmit() {
const { navigate } = this.props.navigation;
return () => {
const formData = new FormData();
formData.append("post[title]", this.state.title);
formData.append("post[body]", this.state.body);
formData.append("post[user_id]", this.props.currentUser.id);
formData.append("post[image]", this.state.image);
this.props.createPost(formData).then((res) => {
if (res.type) {
navigate("Explore");
}
});
};
}
ImagePicker to get an image uri to display a preview as well as update the image uri in the parent component which is the PostForm:
_pickImage = async () => {
let pickerResult = await ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
});
if (pickerResult.cancelled) {
return;
}
this.setState({image: pickerResult.uri});
this.props.updatePostWithImage(pickerResult.uri);
};
Action and API Call:
export const createPost = (post) => dispatch => (
postPost(post).then((res) => {
return dispatch(receivePost(res.data));
}).catch((errors) => {
})
);
const url = "http://localhost:3000";
export const postPost = (post) => {
return axios({
method: 'POST',
url: `${url}/api/posts`,
dataType: "JSON",
contentType: false,
processData: false,
data: post
});
};
Post Controller:
def create
#post = Post.new(post_params)
if #post.save
render :show
else
render json: #post.errors.full_messages, status: 422
end
end
def post_params
params.require(:post).permit(:title, :body, :image, :user_id)
end
Post Model:
class Post < ApplicationRecord
validates :title, :body, presence: true
belongs_to :user
has_attached_file :image, default_url: "https://res.cloudinary.com/jun/image/upload/v1506659435/Doge_sggjpf.jpg"
validates_attachment_content_type :image, content_type: /\Aimage\/.*\z/
end
Parameters that Rails receives with Axios POST request:
Parameters: {"post"=>{"title"=>"Test", "body"=>"Testing", "user_id"=>"1", "image"=>"file:///Users/jimmy/Library/Developer/CoreSimulator/Devices/A3590E6A-281B-4EFB-868C-9F5311718832/data/Containers/Data/Application/CB5ED945-AA87-4E36-83B6-71426759C70C/Library/Caches/ExponentExperienceData/%2540anonymous%252Fartis-ed15f4f4-b8d6-460f-a1b3-e06d546eda2a/ImagePicker/B3162237-9BCD-4721-BDF2-D66BC047A6C0.jpg"}}
I'm not really sure what's going on. The parameters looks fine to me, but I think it might be missing a lot of things that paperclip needs I would assume. But I'm not sure how to get them. I tried searching all over but couldn't find a solution that could help. I'm still fairly new to using some of these technologies, so please bear with me, haha.
If there is any other information I can add in to help debug this issue let me know. Thanks in advance!
Welp, I found the issue and was able to fix it. I was missing additional information that Paperclip required on the back end. The only change I made was on the onSubmit handler function.
onSubmit() {
const { navigate } = this.props.navigation;
return () => {
let uriParts = this.state.image.split('.');
let fileType = uriParts[uriParts.length - 1];
console.log(fileType);
const formData = new FormData();
formData.append("post[title]", this.state.title);
formData.append("post[body]", this.state.body);
formData.append("post[user_id]", this.props.currentUser.id);
formData.append("post[image]", {
uri: this.state.image,
name: `${this.state.title}.${fileType}`,
type: `image/${fileType}`,
});
this.props.createPost(formData).then((res) => {
if (res.type) {
navigate("Explore");
}
});
};
}
Setting the uri, name and file type for the image fixed everything. I can now select an image from the camera roll and successfully create a post using that image, which gets uploaded to AWS S3 using paperclip. :D
Related
I want users to be able to upload a photo on my project and have it added to their profile. I think I might be doing the axios call wrong, because I've tried figuring out how to do this based on a mix of other stack overflow answers.
Here is the onSubmit handler for the small form:
const handleSubmit = (e) => {
e.preventDefault();
const user = { user: avatarUser };
const formData = new FormData();
formData.append('user[profpic]', file)
formData.append('_method', 'PATCH')
user.user.profpic = formData;
addFileToProfile(user).then(closeModal());
};
Here's the axios call:
export const addFileToProfile = (user) =>
axios
.post(`/api/users/${user.user.id}`, user, {
headers: { 'Content-Type': 'multipart/form-data' },
})
The controller is pretty standard, I'm using strong params so that's why I'm nesting the data under the key of user. I have been using routes for creating and patching users successfully, so I don't think the controller is the issue. I'll post the update action just in case, but it is working for things other than this:
def update
#user = User.find(params[:id])
if #user.update(user_params)
render "api/users/show"
else
render json: #user.errors.full_messages, status: 422
end
end
My server logs are pretty suspicious; it looks like for some reason what I sent using axios triggered a GET request:
Started POST "/api/users/5" for ::1 at 2021-11-27 22:24:06 -0500
Started GET "/[object%20FormData]" for ::1 at 2021-11-27 22:24:06 -0500
That's about it. I hope someone with more experience than me can interpret the logs there and figure out what I did wrong.
It looks like the issue was mostly lying in too much nesting in the handleSubmit function. I would recommend making it simpler and passing along nothing but the ID and the formData. I also added another header to the axios call. It's now working as expected!
const handleSubmit = (e) => {
e.preventDefault();
const formData = new FormData();
formData.append(`user[${photoType}]`, file)
const user = { data: formData, id: avatarUser.id };
upload(user).then(closeModal());
};
export const addFileToProfile = (user) => (
axios
.patch(`/api/users/${user.id}`, user.data, {
headers: { "Content-Type": "multipart/form-data", processData: "false" },
})
.then((res) => res.data)
)
I am using Rails and React with Axios to create a record. In my React app I collect all the data and put it inside of FormData like this:
const createVtc = () => {
let data = new FormData()
data.append('image', vtcImageToSend)
data.append('name', vtcName)
data.append('description', vtcDescription)
data.append('main_color', vtcColor)
data.append('minimum_age_to_join', vtcMinimumAge)
axios.post(`${ROOT_API}/v1/vtcs/create`, data, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'multipart/form-data'
}
}).then(res => {
console.log(res.data);
history.push('/dashboard')
}).catch(err => {
console.log(err);
})
};
This contains all the necessary data in order to create a record.
This is the Rails controller responsible for creating it:
def create
vtc = Vtc.new(vtc_params)
# other code is not important
end
And this is vtc_params private function:
def vtc_params
params.require(:vtc).permit(:id, :name, :description, :minimum_age_to_join, :main_color, :image)
end
Pretty standard stuff. It worked until I had to implement picture upload which made me switch to FormData upload and since then Rails throws this error:
ActionController::ParameterMissing in V1::VtcsController#create
param is missing or the value is empty: vtc
I can assume what's the problem but I don't know how to fix it. Before FormData I used to send it like this:
// other stuff
axios.post(`${ROOT_API}/v1/vtcs/create`, {
"vtc": {
"name": vtcName,
// etc.
}
}, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'multipart/form-data'
}
})
// other stuff
All of the data was inside of "vtc" object but now it's just data variable. I tried adding {"vtc": data} as Axios data which not surprisingly didn't work.
Just wrap your data variable in an object with the key vtc:
axios.post(`${ROOT_API}/v1/vtcs/create`, {vtc: data}, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'multipart/form-data'
}
I am trying to issue pre-signed URLs over my server and then upload over javascript in the browser. Everything works when I leave out the :success_action_status field but I want to set it to 201 to get back the XML after uploading.
On the Server:
s3_bucket = Aws::S3::Resource.new.bucket(UploadFile::DECK_BUCKET)
presigned_url = s3_bucket.presigned_post(
:key => #upload_file.key,
:content_length_range => 1..(10*1024),
:success_action_status => '201',
:signature_expiration => expire
)
data = { url: presigned_url.url, url_fields: presigned_url.fields }
render json: data, status: :ok
On the client:
this.file.change(function() {
var formData = new FormData();
formData.append("key", that.fields.key);
formData.append("X-Amz-Credential", that.fields['x-amz-credential']);
formData.append("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
formData.append("X-Amz-Date", that.fields['x-amz-date']);
formData.append("Policy", that.fields.policy);
formData.append("X-Amz-Signature", that.fields['x-amz-signature']);
formData.append("file", that.file[0].files[0]);
formData.append("success_action_status", that.fields['success_action_status']);
that.$http.post(that.url, formData).then(function(response) {
console.log("yup")
console.log(response)
}, function(response) {
console.log("nope")
console.log(response)
});
Again it works when I leave off the success_action_status field in presigned_post. But when I do not I get:
Invalid according to Policy: Policy Condition failed: ["eq", "$success_action_status", "201"]
Anyone know what's going on?? Thanks!
SOLUTION:
formData.append("file", that.file[0].files[0]); must be the last thing appended to the form.
There doesn't appear to be anything specific in the documentation as to why this wouldn't work.
Update
Try putting success_action_status field before the file field
this.file.change(function() {
var formData = new FormData();
formData.append("key", that.fields.key);
formData.append("X-Amz-Credential", that.fields['x-amz-credential']);
formData.append("X-Amz-Algorithm", "AWS4-HMAC-SHA256");
formData.append("X-Amz-Date", that.fields['x-amz-date']);
formData.append("Policy", that.fields.policy);
formData.append("X-Amz-Signature", that.fields['x-amz-signature']);
formData.append("success_action_status", that.fields['success_action_status']);
formData.append("file", that.file[0].files[0]);
that.$http.post(that.url, formData).then(function(response) {
console.log("yup")
console.log(response)
}, function(response) {
console.log("nope")
console.log(response)
});
Quite new to React on Rails apps, especially the React portion. I'm trying to access data in a nested hash that is given from a SQL query in a Rails service. First off, is this even possible?
In Rails Console, lets say user1 has already been found by id, LedgersService.transactions(user1).first returns all data in this format:
{:transactable=>{:type=>"Deposit",
:id=>"28cba04f-5b9d-4c9c-afca-b09a6e0e8739",
:user_id=>"72700244-e6b0-4baf-a381-c22bfe56b022",
:transacted_at=>"2019-03-12 19:04:48.715678", :amount_cents=>15,
:notes=>"none", :processor=>nil, :details=>nil},
:ledgers=>[{:entry_type=>"credit", :amount_cents=>15,
:transacted_at=>"2019-03-12 19:04:48.715678",
:user_id=>"72700244-e6b0-4baf-a381-c22bfe56b022",
:transactable_type=>"Deposit",
:transactable_id=>"28cba04f-5b9d-4c9c-afca-b09a6e0e8739"}]}
I am attempting to do something similar in my React component to try to get the data, however, I'm not quite sure how to set LedgersService.transactions portion. This is how I currently have it:
class LedgersIndex extends React.Component {
constructor(props) {
super(props);
this.state = { ledgers_service: { transactions: [] }, paginator: { count: 0, page: 0, limit: 0 }, user: { id: this.props.match.params.user_id } };
My endpoint call:
componentDidMount() {
var user_id = this.state.user.id;
this.fetchData(user_id, 1);
}
fetchData = (user_id, page_number) => {
apiService.ledgersIndex(user_id, page_number)
.then(
paginated => {
this.setState({
ledgers_service: {
transactions: paginated.ledgers_service.transactions
},
paginator: {
limit: paginated.meta.limit,
count: paginated.meta.count,
page: paginated.meta.page -1
}
});
},
Further down in my render:
render() {
const { classes } = this.props;
const { ledgers_service, paginator } = this.state;
My fetch in apiService:
function locationsIndex(page_number) {
const requestOptions = {
method: 'GET',
headers: Object.assign({},
authorizationHeader(),
{ 'Content-Type': 'application/json' })
};
return fetch(`${process.env.API_SERVER}/api/v1/admin/locations?page=${page_number}`, requestOptions)
.then(handleResponse)
.then(paginated => {
return paginated;
});
}
When I console.log(ledgers_service.transactions(this.state.user.id)), I get the error that ledgers_service.transactions is not a function. console.log(paginator.count) however worked, is this because transactions is being set to an array?
What's the correct way to get that same endpoint in my React component that I got from my rails console?
Quite new to React on Rails apps, especially the React portion. I'm
trying to access data in a nested hash that is given from a SQL query
in a Rails service.
Yes, JS likes JSON so you should have a Rails action that responds with JSON. This is the correct way to exchange data between React and Rails:
# in your React app
fetch('/path/to/resource.json')
.then((returnedResource) => {
// do something with JSON
})
.catch(error => console.error('Error:', error));
# in your controller
respond_to do |format|
format.json { render json: LedgersService.transactions(user1).first }
end
From there, you can treat your returnedResource as a JSON object. In your case, this would be pagination
I'm having trouble understanding how a local file path from a smartphone could possibly get uploaded on the server side with a Rails api for instance.
The file path that we're sending to the backend doesn't mean anything to the server?
I'm getting a uri from the response like this:
file:///Users/.../Documents/images/5249F841-388B-478D-A0CB-2E1BF5511DA5.jpg):
I have tried to send something like this to the server:
let apiUrl = 'https://vnjldf.ngrok.io/api/update_photo'
let uriParts = uri.split('.');
let fileType = uri[uri.length - 1];
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileType}`,
type: `image/${fileType}`,
});
let options = {
method: 'POST',
body: formData,
headers: {
Accept: 'application/json',
'Content-Type': 'multipart/form-data',
},
};
But I'm unsure what it is and how to decript it on the backend.
I have also tried sending the uri direclty but of course I'm getting the following error:
Errno::ENOENT (No such file or directory # rb_sysopen -...
Any help/guidance would be much appreciated.
I have recently spent 1+ hour debugging something similar.
I found out that if you make a POST to your Rails backend from your React Native app using this json:
let formData = new FormData();
formData.append('photo', {
uri,
name: `photo.${fileName}`,
type: `image/${fileType}`,
});
Rails will automatically give you a ActionDispatch::Http::UploadedFile in your params[:photo], which you can attach directly to your model like Photo.create(photo: params[:photo]) and it simply works.
However, if you don't pass a filename, everything breaks and you'll get a huge string instead and it will raise a ArgumentError (invalid byte sequence in UTF-8).
So, based on your code, I can spot the bug right on: you are passing name as photo.${fileType}, which is wrong, and should be photo.${fileName} (update accordingly to get your image filename ... console.log(photo) in your React Native code will show you the correct one.
Maintain issues with deleting and adding new files
This is how I managed to do it add multiple file upload and maintain issues with deleting and adding new files
class User < ApplicationRecord
attribute :photos_urls # define it as an attribute so that seriallizer grabs it to generate JSON i.e. as_json method
has_many_attached :photos
def photos_urls
photos.map do |ip|
{url: Rails.application.routes.url_helpers.url_for(ip), signed_id: ip.signed_id}
end
end
See about signed_id here. It describes how you can handle multiple file upload.
Controller looks like
def update
user = User.find(params[:id])
if user.update(user_params)
render json: {
user: user.as_json(except: [:otp, :otp_expiry])
}, status: :ok
else
render json: { error: user.errors.full_messages.join(',') }, status: :bad_request
end
end
...
private
def user_params
params.permit(
:id, :name, :email, :username, :country, :address, :dob, :gender,
photos: []
)
end
React Native part
I am using react-native-image-crop-picker
import ImagePicker from 'react-native-image-crop-picker';
...
const photoHandler = index => {
ImagePicker.openPicker({
width: 300,
height: 400,
multiple: true,
}).then(selImages => {
if (selImages && selImages.length == 1) {
// Make sure, changes apply to that image-placeholder only which receives 'onPress' event
// Using 'index' to determine that
let output = images.slice();
output[index] = {
url: selImages[0].path, // For <Image> component's 'source' field
uri: selImages[0].path, // for FormData to upload
type: selImages[0].mime,
name: selImages[0].filename,
};
setImages(output);
} else {
setImages(
selImages.map(image => ({
url: image.path, // For <Image> component's 'source' field
uri: image.path, // for FormData to upload
type: image.mime,
name: image.filename,
})),
);
}
});
};
...
<View style={style.imageGroup}>
{images.map((item, index) => (
<TouchableOpacity
key={`img-${index}`}
style={style.imageWrapper}
onPress={() => photoHandler(index)}>
<Image style={style.tileImage} source={item} />
</TouchableOpacity>
))}
</View>
Uploader looks like
// ../models/api/index.js
// Update User
export const updateUser = async ({ id, data }) => {
// See https://developer.mozilla.org/en-US/docs/Web/API/FormData/append
let formData = new FormData(data);
for (let key in data) {
if (Array.isArray(data[key])) {
// If it happens to be an Image field with multiple support
for (let image in data[key]) {
if (data[key][image]?.signed_id) {
// if the data has not change and it is as it was downloaded from server then
// it means you do not need to delete it
// For perverving it in DB you need to send `signed_id`
formData.append(`${key}[]`, data[key][image].signed_id);
} else if (data[key][image]?.uri && data[key][image]?.url) {
// if the data has change and it is as it has been replaced because user selected a different image in place
// it means you need to delete it and replace it with new one
// For deleting it in DB you should not send `signed_id`
formData.append(`${key}[]`, data[key][image]);
}
}
} else {
formData.append(key, data[key]);
}
}
return axios.patch(BASE_URL + "/users/" + data.id, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
};
and Saga worker looks like
import * as Api from "../models/api";
// worker Saga:
function* updateUserSaga({ payload }) {
console.log('updateUserSaga: payload', payload);
try {
const response = yield call(Api.updateUser, {
id: payload.id,
data: payload,
});
if (response.status == 200) {
yield put(userActions.updateUserSuccess(response.data));
RootNavigation.navigate('HomeScreen');
} else {
yield put(userActions.updateUserFailure({ error: response.data.error }));
}
} catch (e) {
console.error('Error: ', e);
yield put(
userActions.updateUserFailure({
error: "Network Error: Could not send OTP, Please try again.",
})
);
}
}