ammarahm-ed / react-native-scoped-storage

MIT License
59 stars 10 forks source link

Is it possible to copy a file from normal Android file storage into scoped storage? #33

Open mitchdowney opened 2 years ago

mitchdowney commented 2 years ago

Hello, sorry as I should probably be able to figure this out on my own...but it is possible to call copyFile to move a file from a normal file storage directory, into a scoped storage directory?

By "normal" file storage directory I mean files saved to the paths that react-native-fs provides:

MainBundlePath (String) The absolute path to the main bundle directory (not available on Android) CachesDirectoryPath (String) The absolute path to the caches directory ExternalCachesDirectoryPath (String) The absolute path to the external caches directory (android only) DocumentDirectoryPath (String) The absolute path to the document directory DownloadDirectoryPath (String) The absolute path to the download directory (on android only) TemporaryDirectoryPath (String) The absolute path to the temporary directory (falls back to Caching-Directory on Android) LibraryDirectoryPath (String) The absolute path to the NSLibraryDirectory (iOS only) ExternalDirectoryPath (String) The absolute path to the external files, shared directory (android only) ExternalStorageDirectoryPath (String) The absolute path to the external storage, shared directory (android only)

Whenever I call copyFile using these paths (although I haven't tried all of them yet) I get a read/write permission error, such as:

Error: 'file://storage/emulated/0/Android/data/com.podverse/files/imYqxyt4gk.mp3'does not have permission to read/write

We're using react-native-background-downloader, and I haven't been able to figure out a way to get files downloaded with it into scoped storage...it seems like react-native-background-downloader library just won't work with content:// as a destination, and react-native-scoped-storage can't read files from the paths I listed above...so it seems like we're stuck? No workarounds? Any info or advice would be appreciated.

ambessh commented 2 years ago

@mitchdowney yes it is possible.

copyfile takes three arguments uri of source uri of destination and callback

make sure you provide uri only not path . and the uri should not be document tree selected uri

ammarahm-ed commented 2 years ago

Update to 1.9.3. it supports reading and writing to normal paths such as cache and data directory.

mitchdowney commented 2 years ago

@ambessh hmm I have 1.9.3 installed, and I'm not sure what I'm doing wrong then...

When I call this:

const result = await ScopedStorage.stat('file://data/user/0/com.podverse/files/_qhiTuJQT.mp3')

I get:

Error: 'file://data/user/0/com.podverse/files/_qhiTuJQT.mp3'does not have permission to read/write

Is that an invalid uri? (that is the RNFS.DocumentDirectoryPath btw)

Also, the copyFile call I am attempting is:

await ScopedStorage.copyFile(`file://data/user/0/com.podverse/files/6NvramAVN.mp3`, content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodcasts/document/01F6-AC3F%3APodcasts/6NvramAVN.mp3, () => {})

That doesn't throw an error, but the file doesn't get saved to the SD card either...is there anything you can see I'm doing wrong? I'm not using a callback, but I'm not sure I need to?

AZKZero commented 2 years ago

@ambessh hmm I have 1.9.3 installed, and I'm not sure what I'm doing wrong then...

When I call this:

const result = await ScopedStorage.stat('file://data/user/0/com.podverse/files/_qhiTuJQT.mp3')

I get:

Error: 'file://data/user/0/com.podverse/files/_qhiTuJQT.mp3'does not have permission to read/write

Is that an invalid uri? (that is the RNFS.DocumentDirectoryPath btw)

Also, the copyFile call I am attempting is:

await ScopedStorage.copyFile(`file://data/user/0/com.podverse/files/6NvramAVN.mp3`, content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodcasts/document/01F6-AC3F%3APodcasts/6NvramAVN.mp3, () => {})

That doesn't throw an error, but the file doesn't get saved to the SD card either...is there anything you can see I'm doing wrong? I'm not using a callback, but I'm not sure I need to?

+1

ambessh commented 2 years ago

@mitchdowney during copy file i had the same issue . it was not saving it and the callback was returning nothing. Go to -> node modules/react-native-scoped-storage/android/src/main/java/com/amaarahmed/scopedstorage/RNScopedStorageModule.java search copyFile function in that java file and replace with this

@ReactMethod
    public void copyFile(String path, String dest, Callback callback) {
        String pathToUri = Uri.fromFile(new File(path)).toString();
        AsyncTask.execute(() -> {

            InputStream in = null;
            OutputStream out = null;
            String message = "";

            try {
                if (!exists(pathToUri)) {
                    message = "Source file does not exist";
                    callback.invoke((message));
                    return;
                }
                ParcelFileDescriptor inputDescriptor = reactContext.getContentResolver().openFileDescriptor(Uri.parse(pathToUri), "rw");
                in = new FileInputStream(inputDescriptor.getFileDescriptor());

                if (!exists(dest)) {
                    message = "Destination file does not exist. Please create destination file with createFile.";
                    callback.invoke((message));
                    return;
                }

                ParcelFileDescriptor outputDescriptor = reactContext.getContentResolver().openFileDescriptor(Uri.parse(dest), "rw");
                out = new FileOutputStream(outputDescriptor.getFileDescriptor());

                byte[] buf = new byte[10240];
                int len;
                while ((len = in.read(buf)) > 0) {
                    out.write(buf, 0, len);
                }
            } catch (Exception err) {
                message += err.getLocalizedMessage();
            } finally {
                try {
                    if (in != null) {
                        in.close();
                    }
                    if (out != null) {
                        out.close();
                    }
                } catch (Exception e) {
                    message += e.getLocalizedMessage();
                }
            }

            if (message != "") {
                callback.invoke(message);
            } else {
                callback.invoke();
            }

       });

    }

and now in react javascript dont pass uri in the first argument of copyfile function . just pass normal path . It will work. laters you can create a patch of the package.

mitchdowney commented 2 years ago

@ammarahm-ed I tried your code, updated RNScopedStorageModule.java (verified the changes are used in the react native app), then call this:

await ScopedStorage.copyFile("/storage/emulated/0/Download/lwx0wMYSjP.mp3", "content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodverse/document/01F6-AC3F%3APodverse/lwx0wMYSjP.mp3", (msg) => {
  console.log('ScopedStorage.copyFile msg', msg)
})

But then I get:

ScopedStorage.copyFile msg Failed to open for writing: java.io.FileNotFoundException: open failed: EISDIR (Is a directory)

Any ideas what the "EISDIR is a directory" is about? That path and uri does not seem like a directory to me...

I also tried this:

await ScopedStorage.createFile("content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodverse/document/01F6-AC3F%3APodverse/", `${episode.id}${ext}`, 'audio/mpeg')
await ScopedStorage.copyFile("/storage/emulated/0/Download/lwx0wMYSjP.mp3", "content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodverse/document/01F6-AC3F%3APodverse/lwx0wMYSjP.mp3", (msg) => {
  console.log('ScopedStorage.copyFile msg', msg)
})

And I verified the file is created as expected...but I still get the "is a directory" error.

mitchdowney commented 2 years ago

@ammarahm-ed eureka! copyFile works for me is I use the uri:

content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodverse/document/01F6-AC3F%3APodverse%2Flwx0wMYSjP.mp3

With the URL encoded %2F character instead of /. Not sure why it's necessary...but it's finally saving to the SD card! Thank you for your help 🙏

mitchdowney commented 2 years ago

@ammarahm-ed ahhh...but there's a bug with this...

It seems like the %2F sometimes works, and sometimes throws a "ENOENT No such file or directory" error:

Works:

content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodcasts/document/01F6-AC3F%3APodcasts%2Fesgi0q1Pm.mp3 content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodcasts/document/01F6-AC3F%3APodcasts%2FuJbRmLuc0.mp3

Fails with ENOENT error:

content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodcasts/document/01F6-AC3F%3APodcasts%2FGwHcavR3a.mp3 content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodcasts/document/01F6-AC3F%3APodcasts%2F6gWMTVqam.mp3

I guess there is some kind of URL encoded clash, depending on the first character of the file id?

ambessh commented 2 years ago

@mitchdowney pls dont hardcode it . Get uri from when you create the file .

mitchdowney commented 2 years ago

@ambessh I was getting very buggy and inconsistent results with this...but I think it may have been conflicts caused by identical files already existing in the temporary DocumentDirectoryPath from previous test downloads.

After wiping the app completely and deleting all data, this code seems to work consistently when run in the react-native-background-downloader done method:

if (customLocation) {
    const tempDownloadFileType = await ScopedStorage.stat(origDestination)
    const newFileType = await ScopedStorage.createFile(customLocation, `${episode.id}${ext}`, 'audio/mpeg')
    if (tempDownloadFileType && newFileType) {
      const { uri: tempFileUri } = tempDownloadFileType
      const { uri: newFileUri } = newFileType
      await ScopedStorage.copyFile(tempFileUri, newFileUri, (msg) => {
        console.log('ScopedStorage.copyFile msg', msg)
      })
      await ScopedStorage.deleteFile(tempFileUri)
    }
}

HOWEVER, stat throws a "cannot read/write" permissions error. I had to go into the RNScopedStorageModule and modify the hasPermission function so that it always returns true. This is obviously no good...but why is read/write permission even needed for the DocumentDirectoryPath? Shouldn't it only be "read" permission that is needed? And isn't this a path that we should be able to read/write to by default anyway, even with scoped storage?

It appears the problem is that DocumentDirectoryPath is not returned by the getPersistedUriPermissions function. It seems crazy though if I have to tell the user to somehow manually allow writes to the DocumentDirectoryPath.

ALSO: deleteFile is failing. It says:

Error: Invalid URI: /storage/emulated/0/Android/data/com.podverse/files/M6FsE-t6d.mp3

What's odd to me is this is the URI that is returned by the stat function...so something seems wrong there.

Rohitbarate commented 1 year ago

@ammarahm-ed eureka! copyFile works for me is I use the uri:

content://com.android.externalstorage.documents/tree/01F6-AC3F%3APodverse/document/01F6-AC3F%3APodverse%2Flwx0wMYSjP.mp3

With the URL encoded %2F character instead of /. Not sure why it's necessary...but it's finally saving to the SD card! Thank you for your help 🙏

hey bro , i am facing issue "source file not exist" and i used this uri : 'content://com.android.externalstorage.documents/tree/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses/document/primary%3AAndroid%2Fmedia%2Fcom.whatsapp%2FWhatsApp%2FMedia%2F.Statuses%2F' and this is my copy file function = const copyFileFunction = async (sourceUrl, destUrl) => { const fileStat = await ScopedStoragePackage.stat(sourceUrl) console.log({fileStat}); await ScopedStoragePackage.copyFile(fileStat.uri,destUrl,(res)=>{console.log({res});}) }; could you pls tell me where i am wrong ? Screenshot 2023-07-20 120458