appwrite / sdk-for-web

[READ-ONLY] Official Appwrite Web SDK 🧑

Home Page:https://appwrite.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

πŸ› Bug Report: React Native File Upload

rohankm opened this issue Β· 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("demo@demo.com", "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?

  • I checked and didn't find similar issue

🏒 Have you read the Code of Conduct?

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 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("demo@demo.com", "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",
  },
});

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("demo@demo.com", "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",
  },
});

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

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

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.

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?

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

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();
};