简体   繁体   中英

How to upload local image file on React Native app to Rails api?

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.",
      })
    );
  }
}

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM