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.
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
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.",
})
);
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.