react-native-documents / document-picker

Document Picker for React Native
https://react-native-documents.github.io/
MIT License
1.35k stars 437 forks source link

Access Content uri in android #70

Closed EyalSi closed 2 years ago

EyalSi commented 7 years ago

I'm getting a content uri in android: 'content://com.android.providers.media.documents/document/image%3A99' Decoding it does not work, and it seems that it's impossible to access the file. Does anyone has an idea how to access the file?

10x

Elyx0 commented 7 years ago

Maybe here: https://stackoverflow.com/questions/3401579/get-filename-and-path-from-uri-from-mediastore

aditya-simplecrm commented 6 years ago

Any solution?

dantman commented 6 years ago

URIs returned from Android are DocumentProviders URIs. You need to access them using a ContentResolver. react-native-fs cannot handle DocumentProvider documents, it's only designed to work with files inside the limited areas of the filesystem the app is permitted to access.

Right now react-native-fetch-blob might work, though it actually does a lot wrong so it may not. React Native is going to be getting Blob/DocumentProvider support in the future (0.50 or 0.51 hopefully), so it'll soon be possible to do things from within react-native.

aditya-simplecrm commented 6 years ago

Any alternate solution for file picker and upload to server?

aditya-simplecrm commented 6 years ago

I am able to get base64 from content uri using below code

RNFetchBlob.fs.readFile(this.state.pdf_uri,'base64') // files will an array contains filenames .then((files) => { this.setState({base64Str:files}) // console.log(files) })

I used this https://github.com/wkh237/react-native-fetch-blob package

dantman commented 6 years ago

Yup, that could work for now, but keep in mind these problems with that library: https://github.com/wkh237/react-native-fetch-blob/issues/525#issuecomment-330529369

aditya-simplecrm commented 6 years ago

@dantman Do you have solution of this problem? I am new to react native.

dantman commented 6 years ago

@aditya-simplecrm The "solution" I'm using in the app I'm working on is just to wait another month for RN to get native Blob uploading.

Alternatively if you feel like writing native code you can just write your own upload code in Java and Objective-C/Swift native code instead of JS.

EyalSi commented 6 years ago

There is now a solution for this problem in react-native-fs. Enabling access and copy content:// files -https://github.com/itinance/react-native-fs/pull/395

dantman commented 6 years ago

React Native 0.54.0 has finally been released with Blob support, you don't need any RNFS hacks. Now all we have to do is come up with some documentation on how to use it.

AgNm commented 6 years ago

@dantman can you please let me know how can we use it? Any document for same? I am using below version:

"react": "16.3.0-rc.0" "react-native": "0.54.3"

dantman commented 6 years ago

@AgNm Lookup the how to use fetch(). Fetch the content:// URL you get back from the document provider like you would any other URL. You should be able to get a Blob instance for the Body. The data for this Blob will be kept on the Native side of react. From there you should be able to use the normal web APIs for Blob instances (there may be some React Native specific behaviours you should look at in documentation or source code). I'm not sure what the API for getting the raw data out of the Blob is. But I do know that you can normally use a Blob as the body of a POST or probably the value of a FormData value. In other words, you should be able to fetch() a content:// URL and then upload the Blob you get back, and React Native will do this without ever sending whatever huge amount of data you downloaded over the slow Native<->JS bridge React Native uses.

AgNm commented 6 years ago

@dantman @Elyx0 I am using below code:

            const data = new FormData();
            data.append('name', file.fileName);
            data.append('photo', {
                  uri: uri_path,
                  type: file.type,
                  name: file.fileName
            });

            fetch(`${API.imageUpload}`, {
            method: 'post',
            body: data
            }).then(res => {
                console.log(JSON.stringify(res));
            });

Below is getting printed in console.log:

            {
                "type": "default",
                "status": 200,
                "ok": true,
                "statusText": 200,
                "headers": {
                    "map": {
                        "date": ["Thu, 05 Apr 2018 09:30:01 GMT"],
                        "content-length": ["2"],
                        "x-powered-by": ["ASP.NET"],
                        "x-aspnet-version": ["4.0.30319"],
                        "expires": ["-1"],
                        "content-type": ["application/json; charset=utf-8"],
                        "server": ["Microsoft-IIS/8.0"],
                        "pragma": ["no-cache"],
                        "cache-control": ["no-cache"]
                    }
                },
                "url": "",
                "_bodyInit": {
                    "listeners": {},
                    "isRNFetchBlobPolyfill": true,
                    "multipartBoundary": null,
                    "_ref": "/data/user/0/com.test/files/RNFetchBlob-blobs/blob-zzzqjeyyi7l2qml24yjhs2",
                    "_blobCreated": true,
                    "_closed": false,
                    "cacheName": "blob-zzzqjeyyi7l2qml24yjhs2",
                    "type": "text/plain",
                    "size": 2
                },
                "_bodyBlob": {
                    "listeners": {},
                    "isRNFetchBlobPolyfill": true,
                    "multipartBoundary": null,
                    "_ref": "/data/user/0/com.test/files/RNFetchBlob-blobs/blob-zzzqjeyyi7l2qml24yjhs2",
                    "_blobCreated": true,
                    "_closed": false,
                    "cacheName": "blob-zzzqjeyyi7l2qml24yjhs2",
                    "type": "text/plain",
                    "size": 2
                }
            }

I am not getting how to proceed further. Can you please help.

dantman commented 6 years ago

@AgNm the /RNFetchBlob-blobs/ suggests you are using the unreliable react-native-fetch-blob package's fetch polyfill. Not the React Native 0.54 built-in fetch with Blob support.

Remove react-native-fetch-blob completely if you can. If not, see if you can bypass it and directly make use of the built-in fetch.

regulus33 commented 6 years ago

URIs returned from Android are DocumentProviders URIs. You need to access them using a ContentResolver. react-native-fs cannot handle DocumentProvider documents, it's only designed to work with files inside the limited areas of the filesystem the app is permitted to access.

I'm not sure this is entirely true. Content uris are workable through many built in aspects of react native as(as of 0.53 anyway). For instance: CameraRoll.getPhotos() will return a collection of objects with content:// uris that are accessible and renderable from within React Native's Image component.

There is also no need to fetch this file locally and convert it to a blob before uploading. You can upload files at content:// uris using a properly formatted FormData object as a part of a multipart/form-data fetch.

Something like:

let data = new FormData()
data.append('image', {uri: 'content://path/to/content', type: 'image/png', name: 'name'})
const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'multipart/form-data',
      },
      body: data
    })

works fine for me. if your'e having trouble with a content:// uri make sure its a properly constructed one, a quick way to test is by pasting in your emulator's browser.

jaeyow commented 6 years ago

@regulus33 this is absolutely correct, using a properly formatted FormData object as shown above will upload the file, without needing to convert the uri to a blob. I can confirm that this is correct as of RN v0.53.0

All React Native needs in the data.append call is the 2nd parameter with a uri attribute. See this comment from React Native FormData code :

// The body part is a "blob", which in React Native just means
// an object with a `uri` attribute. Optionally, it can also
// have a `name` and `type` attribute to specify filename and
// content type (cf. web Blob interface.)

However, to get it to work for me - I didn't need to specify 'Content-Type' in the header. If I set it to multipart-form-data like in the above example, I had issues with fetch not setting the boundaries of the files, which results in failed uploads.

So my fetch call is like this:

let data = new FormData()
data.append('image', {uri: 'content://path/to/content', type: 'image/png', name: 'name'})
const response = await fetch(url, {
      method: 'POST',
      body: data
    })

As an aside, if you want to do this in TypeScript, I had a past blog post about this.

regulus33 commented 6 years ago

@jaeyow Great info! When in doubt, scour the source!

smithaitufe commented 6 years ago

What I am looking for is not uploading to a server. I want to be able to access the actual or real path that this library provides. Please is it possible to achieve this without first uploading to a remote server?

dantman commented 6 years ago

@smithaitufe You cannot access the real path, the real path does not belong to your app on either OS. (On iOS you get a local copy, not the real path; On Android you get a content URI to access it through the document provider)

If you want to read the the data, you can fetch() the URI and use the Blob API to interact with the data.

jkunwar commented 6 years ago

@dantman can you please provide an example for file upload. i am using axios

dantman commented 6 years ago

@jkunwar I haven't had a chance to use it in practice yet so I don't have an example. However I have confirmed that fetching a content:// URI from a picker worked. And others have mentioned that you can use content URIs directly with FormData instead of first fetching the URI to get a Blob.

Blob and FormData are standardized browser APIs, examples specific to the picker shouldn't be required.

Any tutorial or documentation for that should work: http://shiya.io/using-fetch-to-upload-a-file/

jkunwar commented 6 years ago

//choose file using document picker

async chooseFile() {
        try {
            const res = await DocumentPicker.pick({
                type: [DocumentPicker.types.allFiles],
            });
            this.setState({ file: res });
        } catch (err) {
            if (DocumentPicker.isCancel(err)) {
                 console.log(err)
            } else {
                throw err;
            }
        }
    }

//submit form data

 nextPressed() {
        var newFile = new FormData();
        newFile.append('title', this.state.value.title);
        newFile.append('tags', this.state.value.tags);
        newFile.append('category', this.state.value.category);
        if (this.state.value.status === true) {
            newFile.append('isPublic', 'public');
        } else {
            newFile.append('isPublic', 'private');
        }
        newFile.append('file', {
            uri: this.state.file.uri,
            type: this.state.file.type,
            name: this.state.file.name
        });
        this.props.addFile(newFile);
    }

//action to upload file

export const addFile = (file) => (dispatch) => {
    try {
        AsyncStorage.getItem('currentUser', (err, user) => {
            var userObj = JSON.parse(user);
            if (userObj.id) {
                const accessToken = userObj.id;
                dispatch({
                    type: FILE_ADDING,
                    payload: 'Adding file'
                })
                axios.post(`${ROOT_URL}/EpechaFiles/upload`,file, {
                    headers: {
                        'Authorization': `${accessToken}`,
                    }
                }).then(response => dispatch({
                    type: FILE_ADDING_SUCCESS,
                    payload: response.data
                })).catch(function (error) {
                    if (error.response) {
                        // The request was made and the server responded with a status code
                        // that falls out of the range of 2xx
                        dispatch({
                            type: FILE_ADDING_FAILURE,
                            payload: error.response.data.error
                        })
                    } else if (error.request) {
                        // The request was made but no response was received
                        // `error.request` is an instance of XMLHttpRequest in the browser and an instance of
                        // http.ClientRequest in node.js
                        console.log(error.request);
                        dispatch({
                            type: FILE_ADDING_FAILURE,
                            payload: 'connection failed'
                        })
                    } else {
                        // Something happened in setting up the request that triggered an Error
                        console.log('Error', error.message);
                    }
                })
            } else {
                dispatch({
                    type: LOADING_FAILED,
                    payload: 'Please login and continue...'
                })
            }
        })
    } catch (error) {
        console.log("error retriveing store current user value on fileList.js , error: " + error);
    }
}

@dantman these are my codes to upload file. would you please look at it and tell me where i am going wrong

dantman commented 6 years ago

@jkunwar I haven't used the FormData+uri mode yet, other people mentioned it, so I can't really help at the moment.

jkunwar commented 6 years ago

@dantman thanks for the response

jaeyow commented 6 years ago

@jkunwar what type of errors are you seeing?

jaeyow commented 6 years ago

@jkunwar I haven't tried with axios + FormData, but I know this works with fetch + FormData. See this issue with React Native + axios + FormData (https://github.com/axios/axios/issues/1321), and I am not sure if this is still an issue, and might be what you are seeing there..

Preethyp commented 6 years ago

I've used RNFS to read file from content uri, but its returning encoded content. I want to send data to rails as multipart with axios. Suggest me to get file content from content uri without encoding.

Purvik commented 5 years ago

@Preethyp Hey facing the same issue here but still not have any solution to get file path from content uri. Do you have solved this issue? Please show me some way to get the solution.

Preethyp commented 5 years ago

@Purvik I couldn't get file content from content uri. so I ended up using base64 from content uri.

Elyx0 commented 5 years ago

Can people in this thread confirm that this pull https://github.com/Elyx0/react-native-document-picker/pull/224 would solve this and won't cause issues?

tkow commented 5 years ago

I can get blob #224 and convert dataUrl encoding by base64 and write file using some FS library of RN. but I have problem that conversion large file (about more than equal 5MB) file to dataUrl fails, and I'm finding why it is caused. I suspect memory leak when using dataUri with data converted . https://stackoverflow.com/questions/695151/data-protocol-url-size-limitations So, I think blob to dataUri may have big disadvantage about limit size of data. Is there any idea, for example, using conversion blob to more right-weighted byte array or something in js side codes. Actually, I'm grad to use blob to fileStream directory though I can't find the way. Otherwise I know this can be achieved writing native code by Java so, I will do that unless any simple solution's found.

Now, I use this method.

export const blobToBase64Url = (
  blob: Blob
): Promise<Base64FormatObject | null> => {
  const fileReader = new FileReader();
  return new Promise((resolve, reject) => {
    fileReader.readAsDataURL(blob);
    fileReader.onloadend = function onloadend() {
      const base64Url = fileReader.result as string;
      const [definition, dataStr] = base64Url.split(';base64,');
      const type = definition.slice(5);
      resolve({
        type,
        dataStr,
        encoding: 'base64'
      });
    };
    fileReader.onerror = function onerror(event) {
      console.error(event);
      fileReader.abort();
      reject(event);
    };
  });
};
dantman commented 5 years ago

@tkow Blob data is stored in the native code area (don't remember if it's memory or cached in the filesystem). In order to get a base64 string, the entire contents of the file has to be transmitted over the Native<->JS bridge and converted into base64, which is a format that takes up more memory than the binary it represents. And because that base64 string is a JS string, the entirety of that data has to be in memory.

While ArrayBuffers may not have the wasted memory issue of base64, I don't believe there are any version of them that works the Blob way where data isn't handled by JS. So even if you can use them you still don't really want to use them because the real issue is that you do not want to transmit the file data over the Native<->JS bridge unless it's absolutely necessary.

That said if you absolutely have to process a large file in JS and transmitting data over the Native<->JS bridge you may want to look into blob.slice(...). That way you can split a file into smaller chunks and process the chunks one at a time. The whole file will still be sent over the bridge which will make this slow, but as long as you remove references to the previous chunk ASAP so the GC can clear it up and you never accumulate the whole file's data in memory you should be able to avoid an OOM.

For uploading, fetch supports Blob natively so you don't need to pass it over the bridge, you can just submit a fetch request with Blob in the body or FormData.

As for the issue where you want to take a Blob and save it to your app's filesystem. Unfortunately that is the one case where the RN ecosystem may need some more work. What you want there is for the native code to copy the file data directly from the native side of the Blob to the filesystem without ever sending the data over the Native<->JS bridge. Unfortunately I don't know of anything that did this when I was working with RNDP in the past. Ideally RNFS would have a method that would accept a Blob created by RNFS and copy/write it to a file. Which would then allow you to continue doing other things to it with RNFS. But I'm not sure whether that has ever been done.

tkow commented 5 years ago

@dantman

I appreciate to have your time. I agree with you and RN blob should be used as formData now.

BTW, The reason why I want to cache files is that stable react-native-firebase supports cached files only. In edge version, I found the blob conversion way in this code, and tested in same way.

https://github.com/invertase/react-native-firebase/blob/fe959bb8424724fbc7ec74f48d09d8a60fb046a7/packages/common/lib/Base64.js

but, this as previously I and you mentioned , inflate bytes and unreasonable processing bytes. So, this won't work when reading large size blob of as same as my case. Eventually, I will write some native bridge patch in my case. Thanks.

sauravjmt commented 5 years ago

@dantman
How to use content:// to send the file with email attachment ? When i use the content:// as path i am getting 'couldn't attach the file' in mail app Thanks

dantman commented 5 years ago

@sauravjmt I have no clue what API you're even using to open the mail app.

sauravjmt commented 5 years ago

@dantman i am using 'react-native-mail' which is using Intent in android to send with email.

with following basic use

Mailer.mail({ subject: 'Testing', recipients: ['], ccRecipients: [], bccRecipients: [], body: 'A Bold Body', isHTML: true, attachment: { path: , // The absolute path of the file from which to read data. type: // Mime Type: jpg, png, doc, ppt, html, pdf, csv name: // Optional: Custom filename for attachment } }, (error, event) => { console.log("ERROR!!!===>",error) console.log("Event!!!===>",event) Alert.alert( error, event, [ {text: 'Ok', onPress: () => console.log('OK: Email Error Response')}, {text: 'Cancel', onPress: () => console.log('CANCEL: Email Error Response')} ], { cancelable: true } ) });

}

dantman commented 5 years ago

@sauravjmt react-native-mail takes filesystem paths and on Android and then uses Uri.fromFile to make a file:// URI to pass in the intent (I guess they just assume that the app on the other end has permissions to read the whole filesystem?).

You will either have to copy the whole file to your app's cache folder and use that path. Or get react-native-mail to optionally accept URIs instead of paths and pass them on directly (assuming that the app on the other end has permissions to read them).

catmans1 commented 5 years ago

My solution is create Android custom native module, and in React Native we call them with: import { NativeModules } from 'react-native';

sytolk commented 4 years ago

How can I zip or unzip choosen file from document picker? https://github.com/mockingbot/react-native-zip-archive#api

if (res.type === 'application/zip') {
 let result = await fetch(res.uri);
 const blob = await result.blob(); //this works but how to save it and get file path from blob??

 const sourcePath = res.uri; // this not work it is content://..
 const targetPath = `${fs.DocumentDirectoryPath}/drivelog.cblite2`;
 const charset = 'UTF-8';

                unzip(sourcePath, targetPath, charset)
                    .then((path) => {
                        console.log(`unzip completed at ${path}`)
                    })
                    .catch((error) => {
                        console.log(error)
                    })
}
demchenkoalex commented 4 years ago

Hi everyone, I have also encountered this problem and didn't find a solution here, so decided to write a small native module in Kotlin which is working in my case. If anyone has this problem you are welcome to try https://github.com/flyerhq/react-native-android-uri-path

[UPD]: See https://github.com/rnmods/react-native-document-picker/issues/70#issuecomment-846883668

djuwon commented 3 years ago

Hi everyone, I have also encountered this problem and didn't find a solution here, so decided to write a small native module in Kotlin which is working in my case. If anyone has this problem you are welcome to try https://github.com/flyerhq/react-native-android-uri-path

I get an invalid synchronous error when using it while debugging. Do you have any idea why? It seems to pull the full path I want though

demchenkoalex commented 3 years ago

Hi @sawndur you can ask questions directly in my package, so we don't bother people here, but the short answer is - I didn't debug any code with this module In it, however if you will write me step by step what are you using - I will try to reproduce it and see if I can do anything. You can also play with the code yourself and make a PR if there are simple solution available.

djuwon commented 3 years ago

Hi @sawndur you can ask questions directly in my package, so we don't bother people here, but the short answer is - I didn't debug any code with this module In it, however if you will write me step by step what are you using - I will try to reproduce it and see if I can do anything. You can also play with the code yourself and make a PR if there are simple solution available.

I kind of made a workaround to suit my needs. Basically, the RNFetchBlob.fs.stat() wouldn't work because the URI getting received was not matching any of the conditions in the function so it just returned null. So I took your code and tweaked it to work in the stat() method and it worked like a charm. I'm getting full filepaths with file info such as size and name as well. I'm not too sure why it threw "Calling synchronous methods on native modules is not supported in Chrome" but it may have had to do with how I was calling it...

Balthazar33 commented 3 years ago

Can the uri you get from DocumentPicker be used to open the file usingLinking.openURL() ? What I'm trying to achieve is to allow the user to select a file from the device, have it displayed on the screen, and when the user clicks on the file, they should be able to see it.

Trying to convert the uri to blob throws an error ( android.os.TransactionTooLargeException: data parcel size 10425488 bytes )

Is there a way to view the file usingLinking.openURL( uri ) ?

AhmedAli125 commented 3 years ago

You can simply pass another property copyTo: 'documentDirectory' in DocumentPicker.pick()/DocumentPicker.pickMultiple() function. You will receive the uri path of the object in fileCopyUri property of results. then you would simply need to iterate the results object and append 'file://' in fileCopyUri property in results elements, because android path start with file///. You also need to decode the path using decodeURIComponent. You can simply do it like this:

let results = await DocumentPicker.pickMultiple({
        type: [...documentType],
        copyTo: 'documentDirectory',
      });

results = results.map(item => ({
        ...item,
        fileCopyUri: `file://${decodeURIComponent(item.fileCopyUri)}`,
      }));

Note: The android device only reads the path which is like this file:///. with 3 slashes

numandev1 commented 3 years ago

you can also use this approach https://github.com/Shobbak/react-native-compressor/issues/25#issuecomment-900309872 this is same as copyTo approach

networkException commented 2 years ago

Trying to fetch the fileCopyUri on Android (with file:// prepended) fails with TypeError: Network request failed (which comes from here), when logging the actual xhr error it says {"isTrusted": false}.

Is there anything I'm missing? Using the wrong fetch (no external library; react native v0.66.2)? Lacking some permissions?

I tried both 'documentDirectory' and 'cachesDirectory'.

The uri also does not look as different from the one emitted by react-native-image-picker:

Image picker: file:///data/user/0/com.APPNAME/cache/rn_image_picker_lib_temp_RANDOMSTRING.jpg Document picker: file:///data/user/0/com.APPNAME/files/RANDOMSTRING/FILENAME.mp4 or file:///data/user/0/com.APPNAME/cache/RANDOMSTRING/FILENAME.mp4

Edit: It appears to be somewhat size related

majirosstefan commented 2 years ago

Yes, larger documents could not be fetched (using the fetch method), no matter if I am using the copyTo parameter or not So, any ideas, how to 'fetch' bigger files ? (100MB+)? Looks quite strange to me, that this was created in 2017, and there are still some issues like this.

networkException commented 2 years ago

I ended up reading the file manually using https://github.com/RonRadtke/react-native-blob-util#file-stream

The following configuration also solved content uri issues on Android and it also works on iOS:

const picked = await DocumentPicker.pickSingle({ mode: 'import' });
let path = picked.uri;

if (path.startsWith('file://')) path = path.substring('file://'.length);

const stream = await ReactNativeBlobUtil.fs.readStream(path, 'base64', chunkSize);

stream.open();
stream.onError(...);
stream.onData(...);
stream.onEnd(...);
WAQAZ0178 commented 2 years ago

i want to upload audio file to firebase its give me error even i store the uri,type, and name in state

error [Error: failed to stat path null because it does not exist or it is not a folder]