sdk-for-web icon indicating copy to clipboard operation
sdk-for-web copied to clipboard

๐Ÿ› Bug Report: React Native File Upload

Open roo12312 opened this issue 3 years ago โ€ข 12 comments

๐Ÿ‘Ÿ Reproduction steps

Im trying to upload files from react native expo

steps to reproduce

yarn create expo-app demo cd demo yarn add appwrite

here is my App.js

import { StatusBar } from "expo-status-bar";
import { Button, StyleSheet, Text, View } from "react-native";

import { Client, Account, Storage } from "appwrite";
const client = new Client();
client
.setEndpoint("https://demo.com/v1") // We set the endpoint, change this if your using another endpoint URL.
.setProject("62aa497432281eaabc7a"); // Your project ID

const account = new Account(client);
const storage = new Storage(client);

export default function App() {
const login = () => {
  const promise = account.createEmailSession("[email protected]", "demodemo");

  promise.then(
    function (response) {
      console.log(response); // Success
    },
    function (error) {
      console.log(error); // Failure
    }
  );
};
const upload = () => {
  const file = new File(["HELOOOO"], "filenamenew.txt", {
    type: "text/plain",
  });
  const promise2 = storage.createFile("userImages", "unique()", file);

  promise2.then(
    function (response) {
      console.log(response); // Success
    },
    function (error) {
      console.log(error); // Failure
    }
  );
};
return (
<View style={styles.container}>
    <Button onPress={login} title="login" />
    <Button onPress={upload} title="upload data" />
  </View>

);
}

const styles = StyleSheet.create({
container: {
  flex: 1,
  backgroundColor: "#fff",
  alignItems: "center",
  justifyContent: "center",
},
});

๐Ÿ‘ Expected behavior

It should upload a file named filenamenew.txt to the specified bucket. The same works properly on react native web. but its not working in with android and ios

๐Ÿ‘Ž Actual Behavior

Network request failed at node_modules@babel\runtime\helpers\construct.js:19:9 in _construct at node_modules@babel\runtime\helpers\wrapNativeSuper.js:26:22 in Wrapper at http://192.168.0.108:19000/node_modules%5Cexpo%5CAppEntry.bundle?platform=android&dev=true&hot=false&strict=false&minify=false:120669:293 in _createSuperInternal at node_modules\appwrite\dist\cjs\sdk.js:72:8 in AppwriteException#constructor at node_modules\appwrite\dist\cjs\sdk.js:391:22 in __awaiter$argument_3 at node_modules@babel\runtime\helpers\regeneratorRuntime.js:86:13 in tryCatch at node_modules@babel\runtime\helpers\regeneratorRuntime.js:66:31 in at node_modules\appwrite\dist\cjs\sdk.js:25:46 in rejected at node_modules\promise\setimmediate\core.js:37:13 in tryCallOne at node_modules\promise\setimmediate\core.js:123:24 in setImmediate$argument_0 at node_modules\react-native\Libraries\Core\Timers\JSTimers.js:248:12 in _allocateCallback$argument_0 at node_modules\react-native\Libraries\Core\Timers\JSTimers.js:112:14 in _callTimer at node_modules\react-native\Libraries\Core\Timers\JSTimers.js:162:14 in _callReactNativeMicrotasksPass at node_modules\react-native\Libraries\Core\Timers\JSTimers.js:413:41 in callReactNativeMicrotasks at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:391:6 in __callReactNativeMicrotasks at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:133:6 in __guard$argument_0 at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:368:10 in __guard at node_modules\react-native\Libraries\BatchedBridge\MessageQueue.js:132:4 in flushedQueue

๐ŸŽฒ Appwrite version

Version 0.10.x

๐Ÿ’ป Operating system

Windows

๐Ÿงฑ Your Environment

No response

๐Ÿ‘€ Have you spent some time to check if this issue has been raised before?

  • [X] I checked and didn't find similar issue

๐Ÿข Have you read the Code of Conduct?

roo12312 avatar Jul 03 '22 18:07 roo12312

Thanks for reporting this! We're still trying to find a resource to replicate this problem and troubleshoot further. We'll report back when we have more info.

stnguyen90 avatar Jul 07 '22 18:07 stnguyen90

@stnguyen90 here is the manual code

it has

  1. Login 2.Sdk upload Data
  2. xhr methods to upload data
  3. axios to upload data

on ios it just creates a empty document with the file name on android it just throws network error

note : need to change the api url

import { Button, StyleSheet, Text, View } from "react-native";
import React from "react";
import { Client, Account, Storage } from "appwrite";
import axios from "axios";
const client = new Client();

client
  .setEndpoint("https://demo.in/v1") // We set the endpoint, change this if your using another endpoint URL.
  .setProject("62aa497432281eaabc7a"); // Your project ID

const account = new Account(client);
const storage = new Storage(client);

export default function App() {
  const login = () => {
    const promise = account.createEmailSession("[email protected]", "demodemo");

    promise.then(
      function (response) {
        console.log(response); // Success
      },
      function (error) {
        console.log(error); // Failure
      }
    );
  };

  const xhrupload = () => {
    console.log("_--------------------------------------_");
    const file = new File(["HELOOOO"], "finnw.txt", {
      type: "text/plain",
    });
    let formData = new FormData();
    formData.append("fileId", "unique()");
    formData.append("file", file);
    console.log("formData", formData);
    const sendData = sendXmlHttpRequest(formData).then(
      function (response) {
        console.log("response", response); // Success
      },
      function (error) {
        console.log("error", error); // Failure
      }
    );
  };
  const sdkupload = () => {
    console.log("_--------------------------------------_");
    const file = new File(["HELOOOO"], "fi.txt", {
      type: "text/plain",
    });

    const promise2 = storage.createFile("userImages", "unique()", file);
    promise2.then(
      function (response) {
        console.log(response); // Success
      },
      function (error) {
        console.log(error); // Failure
      }
    );
    return;
  };
  function sendXmlHttpRequest(data) {
    const xhr = new XMLHttpRequest();

    return new Promise((resolve, reject) => {
      xhr.onreadystatechange = (e) => {
        if (xhr.readyState !== 4) {
          return;
        }
        console.log("xhr.status", xhr);

        if (xhr.status === 201) {
          resolve(JSON.parse(xhr.response));
        } else {
          reject("Request Failed");
        }
      };

      xhr.open(
        "POST",
        "https://demo.in/v1/storage/buckets/userImages/files/"
      );
      xhr.withCredentials = true;
      // xhr.setRequestHeader("content-type", "multipart/form-data");
      xhr.setRequestHeader("X-Appwrite-Project", "62aa497432281eaabc7a");
      xhr.setRequestHeader("X-Appwrite-Response-Format", "0.15.0");
      xhr.setRequestHeader("x-sdk-version", "appwrite:web:9.0.1");
      xhr.send(data);
    });
  }
  const axiosUpload = () => {
    console.log("_--------------------------------------_");
    const file = new File(["HELOOOO"], "fi.txt", {
      type: "text/plain",
    });
    let formData = new FormData();
    formData.append("fileId", "unique()");
    formData.append("file", file);
    console.log("formData", formData);
    axios({
      url: "https://demo.in/v1/storage/buckets/userImages/files/",
      method: "POST",
      data: formData,
      headers: {
        "X-Appwrite-Project": "62aa497432281eaabc7a",
        "X-Appwrite-Response-Format": "0.15.0",
        "x-sdk-version": "appwrite:web:9.0.1",
      },
    })
      .then(function (response) {
        console.log("response :", response);
      })
      .catch(function (error) {
        console.log("error from image :", error);
      });
  };
  return (
    <View style={styles.container}>
      <Button onPress={login} title="login" />
      <Button onPress={sdkupload} title="sdk upload data" />
      <Button onPress={xhrupload} title="xhrupload upload data" />
      <Button onPress={axiosUpload} title="axiosUpload" />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

roo12312 avatar Jul 09 '22 03:07 roo12312

Finally I was able to solve the issue, thanks to @stnguyen90 for helping out

the issue was with the form data handled by the sdk.

here is my code and i have used custom form and XMLHttpRequest() to upload the images

import { Button, Image, StyleSheet, Text, View } from "react-native";
import React, { useState } from "react";
import * as ImagePicker from "expo-image-picker";
import { Client, Account } from "appwrite";
import axios from "axios";
const client = new Client();
const API_URL = "https://demo.in/v1";
const PROJECT_ID = "62aa497432281eaabc7a";
const BUCKET_ID = "userImages";
client
  .setEndpoint(API_URL) // We set the endpoint, change this if your using another endpoint URL.
  .setProject(PROJECT_ID); // Your project ID

const account = new Account(client);

export default function App() {
  const [image, setImage] = useState(null);
  const [succ, setSucc] = useState(false);
  const login = () => {
    const promise = account.createEmailSession("[email protected]", "demodemo");

    promise.then(
      function (response) {
        console.log(response); // Success
      },
      function (error) {
        console.log(error); // Failure
      }
    );
  };

  function sendXmlHttpRequest(data) {
    const xhr = new XMLHttpRequest();

    return new Promise((resolve, reject) => {
      xhr.onreadystatechange = (e) => {
        if (xhr.readyState !== 4) {
          return;
        }
        console.log("xhr.status", xhr);

        if (xhr.status === 201) {
          resolve(JSON.parse(xhr.response));
        } else {
          reject("Request Failed");
        }
      };

      xhr.open("POST", `${API_URL}/v1/storage/buckets/${BUCKET_ID}/files/`);
      xhr.withCredentials = true;
      // xhr.setRequestHeader("content-type", "multipart/form-data");
      xhr.setRequestHeader("X-Appwrite-Project", PROJECT_ID);
      xhr.setRequestHeader("X-Appwrite-Response-Format", "0.15.0");
      xhr.setRequestHeader("x-sdk-version", "appwrite:web:9.0.1");
      xhr.send(data);
    });
  }

  const pickImage = async () => {
    // No permissions request is necessary for launching the image library
    let result = await ImagePicker.launchImageLibraryAsync({
      mediaTypes: ImagePicker.MediaTypeOptions.All,
      allowsEditing: true,
      aspect: [4, 3],
      quality: 1,
    });

    console.log(result);

    if (!result.cancelled) {
      setImage(result.uri);
    }
  };

  const uploadImage = async () => {
    let filename = image.split("/").pop();

    // Infer the type of the image
    let match = /\.(\w+)$/.exec(image);
    let type = match ? `image/${match[1]}` : `image`;

    console.log("_--------------------------------------_");
    let formData = new FormData();
    formData.append("fileId", "unique()");
    formData.append("file", {
      uri: image,
      name: filename,
      type,
    });
    // formData.append("read", "");
    // formData.append("write", "");

    console.log("formData", formData);
    await sendXmlHttpRequest(formData).then(
      function (response) {
        console.log("response", response); // Success
        setSucc(true);
      },
      function (error) {
        console.log("error", error); // Failure
      }
    );
  };

  return (
    <View style={styles.container}>
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        <Button onPress={login} title="login" />
        <Button title="Pick an image from camera roll" onPress={pickImage} />
      </View>
      <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
        {image && (
          <>
            <Image
              source={{ uri: image }}
              style={{ width: 200, height: 200 }}
            />
            <Button onPress={uploadImage} title="uploadImage" />
          </>
        )}
        {succ && <Text style={{ fontSize: 32 }}>UPLOADED</Text>}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
});

roo12312 avatar Jul 09 '22 04:07 roo12312

this is still an issue... the solution above works - for files less than the chunk size of 5mb... any thing larger than that doesn't work...

I've been looking into the module react-native-chunk-upload uploads chunks in the format *.tmp ... not sure if this is the reason why it doesn't work

johnfidel98 avatar Feb 03 '23 19:02 johnfidel98

It's still an issue with the SDK but this is a version of the code above using the fetch api instead of building the XMLHttpRequest and it's working good for my case :

fetch(`${API_URL}/v1/storage/buckets/${BUCKET_ID}/files/`, {
        method: "POST",
        headers: {
            "content-type": "multipart/form-data",
            "X-Appwrite-Project": PROJECT_ID,
            "x-sdk-version": "appwrite:web:10.2.0",
        },
        body: formData,
        credentials: "include",
    });

Badbeef1 avatar Mar 19 '23 14:03 Badbeef1

I've been looking into the module react-native-chunk-upload uploads chunks in the format *.tmp ... not sure if this is the reason why it doesn't work

I tried it and also react-native-background-upload. When it comes to a chunked upload, i get a response code of 400, but only with React Native. The same code in a React Website works.

trashcoder avatar Jun 30 '23 13:06 trashcoder

So, the crux of the problem with our SDK and react-native is FromData + File. In the browser, you can put a File into FormData and the browser will handle the multipart form request fine. React-Native, doesn't handle File the same way which is why you need to make formdata like:

    let formData = new FormData();
    formData.append("fileId", "unique()");
    formData.append("file", {
      uri: image,
      name: filename,
      type,
    });

Fyi, this is what react native expects a file to be: https://github.com/facebook/react-native/blob/17ecae9ce7bded79ab3a083c9d07e15460e5635c/packages/react-native/types/modules/globals.d.ts#L107

Now, we'd like to use the same SDK and for both browser and react-native and we'd like the signature to kind of be the same. I'm not sure what makes sense from the react-native side, though. How else are people getting files besides ImagePicker? And what do you have if not a URI?

stnguyen90 avatar Sep 07 '23 23:09 stnguyen90

So, the crux of the problem with our SDK and react-native is FromData + File. In the browser, you can put a File into FormData and the browser will handle the multipart form request fine. React-Native, doesn't handle File the same way which is why you need to make formdata like:

    let formData = new FormData();
    formData.append("fileId", "unique()");
    formData.append("file", {
      uri: image,
      name: filename,
      type,
    });

Now, we'd like to use the same SDK and for both browser and react-native and we'd like the signature to kind of be the same. I'm not sure what makes sense from the react-native side, though. How else are people getting files besides ImagePicker? And what do you have if not a URI?

It's just file uri unless you want to convert it to a blob

roo12312 avatar Sep 08 '23 00:09 roo12312

btw here is my updated code and it works fine for me

const uploadToStorage = async (
  bucketId,
  uri,
  permissions,
  fileId = "unique()",
  name,
  ftype
) => {
  // Infer the type of the image
  const match = /\.(\w+)$/.exec(uri);
  const filename = name ? `${name}.${match[1]}` : uri.split("/").pop();
  const fileIdP = filename.split("_").pop().split(".").shift();
  const type = ftype ? ftype : match ? `image/${match[1]}` : `image`;

  const formData = new FormData();
  formData.append("fileId", fileIdP);
  formData.append("file", {
    uri: uri,
    name: filename,
    type,
  });
  permissions.forEach((p) => {
    formData.append("permissions[]", p);
  });

  const response = await fetch(
    `${appwrite.config.endpoint}/storage/buckets/${bucketId}/files`,
    {
      method: "POST", // or 'PUT'
      headers: {
        ...appwrite.headers,
        "Content-Type": "multipart/form-data;",
      },
      body: formData,
    }
  );

  return response.json();
};

roo12312 avatar Sep 08 '23 00:09 roo12312

hi , this doesn't work for the new web sdk version, any solution on how to upload images ?

saja25 avatar Feb 20 '24 14:02 saja25

I wish I could help but I migrated to supabase

roo12312 avatar Feb 20 '24 15:02 roo12312

@rohankm We now have React Native SDK. Please check it out at https://github.com/appwrite/sdk-for-react-native

lohanidamodar avatar Mar 08 '24 04:03 lohanidamodar